JSON Progresivo en PHP: Optimiza APIs lentas con Laravel y Symfony

JSON Progresivo
Solucionex
16
Jul 25

¿Tu API tarda demasiado porque una parte de los datos es lenta? ¿Tu frontend React o Vue tiene que esperar todo un json_encode() para empezar a renderizar?

En este artículo aprenderás a aplicar una idea inspirada en los JPEG progresivos y React Server Components, pero usando PHP, tanto con Laravel como con Symfony.

🧠 ¿Qué es JSON progresivo?

JSON progresivo es una estrategia para enviar datos por partes, permitiendo que el cliente empiece a trabajar con lo que ya está disponible, en lugar de esperar todo el bloque de JSON.

Imagina una respuesta típica de una API:

{
  "user": { "name": "Ana" },
  "profile": { "bio": "Desarrolladora", "projects": [...] },
  "footer": "Gracias por visitar"
}

Si projects tarda 2 segundos en consultarse, la respuesta completa se retarda 2 segundos, aunque user y footer ya estén listos.

🎯 Objetivo

Vamos a implementar una técnica para enviar los datos progresivamente, con estructuras como:

{
  "user": "$1",
  "profile": "$2",
  "footer": "$3"
}
/* $1 */
{ "name": "Ana" }
/* $3 */
"Gracias por visitar"
/* $2 */
{ "bio": "Desarrolladora", "projects": "$4" }
/* $4 */
["Proyecto A", "Proyecto B"]

Y en el frontend, podrías reconstruir el JSON reemplazando las referencias $n con sus valores conforme llegan.

🛠️ Caso 1: Laravel — Streaming Progresivo con StreamedResponse

Laravel permite enviar respuestas por partes usando StreamedResponse. Vamos a ver un ejemplo.

✅ Código ejemplo en Laravel

use Symfony\Component\HttpFoundation\StreamedResponse;
Route::get('/api/progressive', function () {
    return new StreamedResponse(function () {
        echo json_encode([
            'user' => '$1',
            'profile' => '$2',
            'footer' => '$3'
        ]) . "\n";
        // Simula datos rápidos
        echo "/* \$1 */\n";
        echo json_encode(['name' => 'Ana']) . "\n";
        echo "/* \$3 */\n";
        echo json_encode("Gracias por visitar") . "\n";
        // Simula operación costosa
        sleep(2);
        echo "/* \$2 */\n";
        echo json_encode([
            'bio' => 'Desarrolladora',
            'projects' => '$4'
        ]) . "\n";
        // Simula otra espera
        sleep(1);
        echo "/* \$4 */\n";
        echo json_encode(['Proyecto A', 'Proyecto B']) . "\n";
    }, 200, [
        'Content-Type' => 'application/json',
        'Transfer-Encoding' => 'chunked',
    ]);
});

Este endpoint responde en streaming y permite al frontend empezar a procesar los datos de forma progresiva.

🛠️ Caso 2: Symfony — Usando StreamedResponse para APIs reactivas

En Symfony, también puedes usar StreamedResponse de forma muy similar.

✅ Código ejemplo en Symfony

use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\Routing\Annotation\Route;
class ApiController extends AbstractController
{
    #[Route('/api/progressive', name: 'api_progressive')]
    public function progressive(): StreamedResponse
    {
        $response = new StreamedResponse(function () {
            echo json_encode([
                'user' => '$1',
                'profile' => '$2',
                'footer' => '$3'
            ]) . "\n";
            echo "/* \$1 */\n";
            echo json_encode(['name' => 'Ana']) . "\n";
            echo "/* \$3 */\n";
            echo json_encode("Gracias por visitar") . "\n";
            sleep(2);
            echo "/* \$2 */\n";
            echo json_encode([
                'bio' => 'Desarrolladora',
                'projects' => '$4'
            ]) . "\n";
            sleep(1);
            echo "/* \$4 */\n";
            echo json_encode(['Proyecto A', 'Proyecto B']) . "\n";
        });
        $response->headers->set('Content-Type', 'application/json');
        $response->headers->set('Transfer-Encoding', 'chunked');
        return $response;
    }
}

⚙️ Cómo consumirlo en el frontend

El frontend debe reconstruir el JSON progresivamente. Puedes usar ReadableStream en el navegador, o una librería como rxjs o incluso fetch con reader.

Ejemplo (simplificado en JS):

const decoder = new TextDecoder();
const response = await fetch('/api/progressive');
const reader = response.body.getReader();
let buffer = '';
const chunks = {};
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  buffer += decoder.decode(value, { stream: true });
  while (buffer.includes("\n")) {
    const [line, ...rest] = buffer.split("\n");
    buffer = rest.join("\n");
    if (line.startsWith('/* $')) {
      const key = line.match(/\$[0-9]+/)[0];
      const valueLine = buffer.split("\n")[0];
      chunks[key] = JSON.parse(valueLine);
      buffer = buffer.split("\n").slice(1).join("\n");
    }
  }
}
// Reconstruir árbol final...

🧠 Conclusiones

  • Puedes usar streaming de JSON en PHP usando StreamedResponse tanto en Laravel como Symfony.
  • Este enfoque es ideal cuando algunas partes de la respuesta son lentas, pero otras están disponibles inmediatamente.
  • Necesitas una forma en el cliente para reconstruir y manejar los datos progresivamente (con placeholders como Promises o similar).

❓ ¿Cuándo aplicar esto?

✅ Ideal para:

  • APIs de contenido que se genera dinámicamente.
  • Interfaces que usan React, Vue o similares con capacidad de streaming.
  • Sistemas donde un cuello de botella puede retrasar toda la respuesta.

❌ Evitar en:

  • APIs pequeñas o muy rápidas.
  • Casos donde el cliente no puede manejar respuestas parciales.

 

Artículo basado en Progressive JSON por Dan Abramov