Ir al contenido

Restricciones del streaming HTML en una sola pasada (ADR-001)

NextPDF renderiza HTML en una única pasada hacia adelante y no conserva ningún árbol de elementos en memoria. ADR-001 registra esta decisión y las restricciones que impone a cada funcionalidad de CSS.

Ventana de terminal
composer require nextpdf/core:^3

El subsistema de HTML es un renderer de HTML+CSS a PDF mediante streaming de una sola pasada. ADR-001 («Stream-based Rendering Pipeline Retention», aceptada el 2026-04-06) es la decisión arquitectónica que fija este modelo. Esta página explica qué es el modelo, qué no hace y qué restricciones impone a quienes contribuyen.

En el modelo de streaming, el tokenizador (HtmlTokenizer) lee la entrada una sola vez y produce una lista plana de tokens. HtmlParser::processTokens() recorre esa lista de izquierda a derecha. Va escribiendo operadores del flujo de contenido PDF en un búfer de cadena al llegar a cada elemento. El motor no construye ningún grafo de elementos persistente entre llamadas. El estado que debe sobrevivir a una llamada de handler se transporta mediante un objeto de valor que actúa como instantánea (HtmlBlockCursor), no mediante nodos compartidos. La herencia de estilos usa una pila de instancias planas de HtmlStyleState, con operaciones de push y pop, no un árbol con punteros al padre.

Este no es un modelo de documento retenido. El motor no conserva un árbol de documento, no vuelve a maquetar el contenido que ya ha escrito y no permite que la entrada cambie una vez iniciado el análisis. La frontera es clara: NextPDF hace streaming de extremo a extremo. Un renderer retenido construye primero todo el documento en memoria; NextPDF no lo hace.

Dos operaciones necesitan un lookahead limitado y son excepciones explícitas y acotadas. El dimensionado de columnas de tabla escanea cada fila antes de colocar una celda. Para ello, almacena esas filas en un búfer de tabla efímero dentro de TableParser, una excepción que ADR-001 reconoce por su nombre. El selector relacional :has() y los selectores :last-child y :last-of-type usan un preescaneo acotado sobre la lista plana de tokens, no un recorrido de árbol. ADR-001 registra ambas excepciones y las delimita.

El modelo es seguro en workers. HtmlParser se construye una vez por solicitud, nunca como singleton. HtmlParser::parse() restablece cada campo al inicio de cada llamada. No existe estado mutable estático en la ruta de renderizado, por lo que RoadRunner, Swoole y Laravel Octane pueden reutilizar el proceso sin que el estado se filtre entre documentos.

Estos símbolos imponen las restricciones que se indican a continuación. Comprueba cada uno en src/Html/.

SímboloUbicaciónFunción
HtmlParser::parse(string $html): HtmlRenderResultsrc/Html/HtmlParser.phpPunto de entrada. Restablece todo el estado y luego ejecuta una única pasada.
HtmlParser::MAX_ELEMENT_COUNT (50_000)src/Html/HtmlParser.phpLímite estricto de elementos procesados.
HtmlParser::MAX_NESTING_DEPTH (100)src/Html/HtmlParser.phpLímite estricto de profundidad de anidamiento.
HtmlBlockCursorsrc/Html/HtmlBlockCursor.phpInstantánea del cursor. El único mecanismo de estado compartido.
HtmlStyleStatesrc/Html/HtmlStyleState.phpMarco de estilo apilado. Sin puntero al padre.
TableParser::reset()src/Html/TableParser.phpRestablecimiento obligatorio del búfer de tabla efímero entre tablas.

El modelo de streaming es invisible para quien llama. Una sola llamada renderiza cualquier documento compatible.

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Core\Document;
$doc = Document::createStandalone();
$doc->setTitle('Streaming render');
$doc->addPage();
$doc->writeHtml('<h1>One forward pass</h1><p>No retained tree.</p>');
$doc->save(__DIR__ . '/output/streaming.pdf');

Renderiza un documento grande con un presupuesto de memoria fijo. El límite de elementos es el límite de seguridad; dimensiona la entrada antes de la llamada.

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Core\Document;
use NextPDF\Exception\HtmlParsingException;
/**
* Render trusted HTML, surfacing the streaming-model limits as typed errors.
*
* @param non-empty-string $html
*/
function renderReport(string $html, string $out): void
{
$doc = Document::createStandalone();
$doc->addPage();
try {
$doc->writeHtml($html);
} catch (HtmlParsingException $e) {
// Thrown on the 10 MB input cap, the 50,000-element cap,
// or the 100-level nesting cap. These are model boundaries,
// not transient faults — do not retry.
throw $e;
}
$doc->save($out);
}
  • El límite de elementos se aplica de forma estricta. El motor lanza HtmlParsingException en MAX_ELEMENT_COUNT = 50_000. Divide los informes muy grandes en varias llamadas a writeHtml() o en varios documentos.
  • El límite de anidamiento se aplica de forma estricta. Una profundidad por encima de MAX_NESTING_DEPTH = 100 lanza una excepción. Los envoltorios profundamente anidados son la causa habitual.
  • Límite de tamaño de entrada. HtmlParser::parse() rechaza entradas superiores a 10 MB antes de tokenizar.
  • :has() es condicional. El preescaneo de :has() solo se ejecuta cuando la función experimental css.has está activa. Sin ella, los selectores :has() no coinciden.
  • El almacenamiento en búfer de tablas es el único árbol efímero. Una sola tabla muy ancha o muy alta mantiene sus filas en memoria hasta render(). TableParser acota este búfer por tabla y lo restablece entre tablas; no es un árbol que abarque todo el documento.
  • Sin remaquetación. El contenido que ya se ha escrito nunca se mueve. Un estilo tardío no puede cambiar de forma retroactiva la salida anterior.

El modelo de streaming mantiene como máximo un HtmlStyleState por nivel de anidamiento, acotado por MAX_NESTING_DEPTH = 100, más los campos del cursor activo. La memoria del estado de estilos y del cursor es O(profundidad), no O(número de elementos). ADR-001 registra la intención de diseño de que este consumo se mantenga muy por debajo del de un grafo de objetos retenido para la misma entrada. El benchmark controlado de RSS máximo con 50,000 elementos es el objetivo de validación empírica nombrado en ADR-001. Se hace seguimiento mediante el benchmark de rendimiento del pipeline de renderizado HTML y su umbral de regresión del 5 % (trabajo fusionado, PR #564). Trata el performance_budget por página (wall_ms: 1500, peak_mb: 64) como el techo operativo.

Los límites de esta página también son controles contra denegación de servicio. DefaultHtmlSecurityPolicy impone un techo de entrada de 10 MB y el techo de anidamiento de 100 niveles de forma independiente del analizador, de modo que un documento hostil no puede agotar la memoria por profundidad o tamaño. El modelo de streaming en sí acota la memoria por construcción: no hay ningún grafo de elementos que un atacante pueda inflar. Consulta el modelo de seguridad del módulo HTML y los contratos de capa para conocer la superficie completa de la política.

Esta página no cita ninguna norma externa. Las restricciones derivan de ADR-001 y de los símbolos de código fuente que las imponen, enumerados en la superficie de la API. Las correspondencias de comportamiento con la especificación CSS están documentadas en css-resolver, no aquí.

Capacidad Enterprise. La arquitectura de streaming es idéntica en Core y Premium. Premium amplía la cobertura de CSS; no cambia el modelo de una sola pasada ni relaja estos límites. Consulta la matriz de compatibilidad CSS.