Pular para o conteúdo

Restrições do streaming de passagem única em HTML (ADR-001)

O NextPDF renderiza HyperText Markup Language (HTML) em uma única passagem, sempre para frente, e não mantém nenhuma árvore de elementos na memória. A ADR-001 registra essa escolha e as restrições que ela impõe a cada recurso de Cascading Style Sheets (CSS).

Terminal window
composer require nextpdf/core:^3

O subsistema HTML renderiza HTML e CSS para Portable Document Format (PDF) em uma única passagem em streaming. A ADR-001 (“Stream-based Rendering Pipeline Retention”, aceita em 2026-04-06) é a decisão de arquitetura que define esse modelo. Esta página explica o modelo, seus limites e as restrições que os contribuidores devem respeitar.

Nesse modelo, o tokenizador (HtmlTokenizer) lê a entrada uma vez e produz uma lista plana de tokens. HtmlParser::processTokens() percorre essa lista da esquerda para a direita e grava os operadores de content stream do PDF em um buffer de string à medida que chega a cada elemento. O motor não constrói nenhum grafo de elementos persistente entre chamadas. Qualquer estado que precise sobreviver a uma chamada de handler passa por um value object de snapshot (HtmlBlockCursor), não por nós compartilhados. A herança de estilos usa push e pop em uma pilha de instâncias planas de HtmlStyleState, não uma árvore de ponteiros para o pai.

Este não é um modelo de documento retido. O motor não mantém uma árvore de documento, não refaz o layout do conteúdo que já gravou e não permite que a entrada sofra mutação depois que a análise começa. O limite é claro: o NextPDF faz streaming de ponta a ponta. Um renderizador retido constrói primeiro o documento inteiro na memória; o NextPDF não faz isso.

Duas operações precisam de lookahead limitado. Ambas são exceções explícitas e limitadas. O dimensionamento de colunas de tabela varre todas as linhas antes de posicionar uma célula. Esse processo armazena essas linhas em um buffer de tabela efêmero dentro do TableParser, uma exceção que a ADR-001 reconhece explicitamente. O seletor relacional :has() e os seletores :last-child e :last-of-type usam uma pré-varredura limitada sobre a lista plana de tokens, não um percurso de árvore. A ADR-001 registra ambas as exceções e seus limites.

O modelo é seguro para uso com workers. O HtmlParser é construído uma vez por requisição, nunca como singleton. O HtmlParser::parse() reinicia todos os campos no início de cada chamada. Não existe estado mutável estático no caminho de renderização; portanto, RoadRunner, Swoole e Laravel Octane podem reutilizar o processo sem que o estado vaze entre documentos.

Os símbolos a seguir aplicam essas restrições. Confira cada um deles em src/Html/.

SímboloLocalFunção
HtmlParser::parse(string $html): HtmlRenderResultsrc/Html/HtmlParser.phpPonto de entrada. Reinicia todo o estado e então executa a passagem única.
HtmlParser::MAX_ELEMENT_COUNT (50_000)src/Html/HtmlParser.phpLimite rígido de elementos processados.
HtmlParser::MAX_NESTING_DEPTH (100)src/Html/HtmlParser.phpLimite rígido de profundidade de aninhamento.
HtmlBlockCursorsrc/Html/HtmlBlockCursor.phpSnapshot do cursor. O único mecanismo de estado compartilhado.
HtmlStyleStatesrc/Html/HtmlStyleState.phpFrame de estilo mantido na pilha. Sem ponteiro para o pai.
TableParser::reset()src/Html/TableParser.phpReinício obrigatório do buffer de tabela efêmero entre tabelas.

Você não gerencia o modelo de streaming diretamente. Uma única chamada renderiza qualquer documento suportado.

<?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');

Renderize um documento grande dentro de um orçamento de memória fixo. O limite de elementos é o limite de segurança; portanto, dimensione a entrada antes da chamada.

<?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);
}
  • O limite de elementos é rígido. O motor lança HtmlParsingException em MAX_ELEMENT_COUNT = 50_000. Divida relatórios muito grandes em várias chamadas de writeHtml() ou em vários documentos.
  • O limite de aninhamento é rígido. Profundidade acima de MAX_NESTING_DEPTH = 100 lança exceção. Wrappers profundamente aninhados costumam causar isso.
  • Limite de tamanho da entrada. O HtmlParser::parse() rejeita entradas maiores que 10 MB antes da tokenização.
  • :has() é condicionado por flag. A pré-varredura de :has() é executada somente quando o recurso experimental css.has está ativo. Sem ele, os seletores :has() não correspondem.
  • O buffering de tabela é a única árvore efêmera. Uma única tabela muito larga ou muito alta mantém suas linhas na memória até render(). O TableParser limita esse buffer por tabela e o reinicia entre tabelas; não é uma árvore que abrange o documento inteiro.
  • Sem refazer o layout. O conteúdo já gravado nunca é movido. Um estilo tardio não pode alterar retroativamente a saída anterior.

O modelo de streaming mantém no máximo um HtmlStyleState por nível de aninhamento, limitado por MAX_NESTING_DEPTH = 100, além dos campos do cursor ativo. A memória de style-state e cursor é O(profundidade), não O(quantidade de elementos). A ADR-001 registra a intenção de projeto de manter isso bem abaixo de um grafo de objetos retido para a mesma entrada. O benchmark controlado de pico de resident set size (RSS) com 50,000 elementos é a meta de validação empírica nomeada na ADR-001. O benchmark de desempenho do render-pipeline de HTML o acompanha com um gate de regressão de 5% (trabalho mesclado, PR #564). Trate o performance_budget por página (wall_ms: 1500, peak_mb: 64) como o teto operacional.

Os limites desta página também atuam como controles contra negação de serviço. O DefaultHtmlSecurityPolicy aplica um teto de entrada de 10 MB e o teto de aninhamento de 100 níveis de forma independente do parser, de modo que um documento hostil não pode esgotar a memória por profundidade ou tamanho. O próprio modelo de streaming limita a memória por construção: não há grafo de elementos para um atacante inflar. Consulte o modelo de segurança do módulo HTML e os contratos de camada para conhecer toda a superfície de políticas.

Esta página não cita nenhuma norma externa. Essas restrições vêm da ADR-001 e dos símbolos de código que as aplicam, listados em Superfície da API. Os mapeamentos comportamentais da especificação de CSS estão documentados em css-resolver, não nesta página.

Recurso Enterprise. A arquitetura de streaming é idêntica no Core e no Premium. O Premium amplia a cobertura de CSS; ele não altera o modelo de passagem única nem relaxa esses limites. Consulte a matriz de suporte a CSS.