Restrições do streaming de passagem única em HTML (ADR-001)
Visão geral
Seção intitulada “Visão geral”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).
Instalação
Seção intitulada “Instalação”composer require nextpdf/core:^3Visão conceitual
Seção intitulada “Visão conceitual”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.
Superfície da API
Seção intitulada “Superfície da API”Os símbolos a seguir aplicam essas restrições. Confira cada um deles em src/Html/.
| Símbolo | Local | Função |
|---|---|---|
HtmlParser::parse(string $html): HtmlRenderResult | src/Html/HtmlParser.php | Ponto de entrada. Reinicia todo o estado e então executa a passagem única. |
HtmlParser::MAX_ELEMENT_COUNT (50_000) | src/Html/HtmlParser.php | Limite rígido de elementos processados. |
HtmlParser::MAX_NESTING_DEPTH (100) | src/Html/HtmlParser.php | Limite rígido de profundidade de aninhamento. |
HtmlBlockCursor | src/Html/HtmlBlockCursor.php | Snapshot do cursor. O único mecanismo de estado compartilhado. |
HtmlStyleState | src/Html/HtmlStyleState.php | Frame de estilo mantido na pilha. Sem ponteiro para o pai. |
TableParser::reset() | src/Html/TableParser.php | Reinício obrigatório do buffer de tabela efêmero entre tabelas. |
Exemplo de código — Início rápido
Seção intitulada “Exemplo de código — Início rápido”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');Exemplo de código — Produção
Seção intitulada “Exemplo de código — Produção”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);}Casos extremos e pegadinhas
Seção intitulada “Casos extremos e pegadinhas”- O limite de elementos é rígido. O motor lança
HtmlParsingExceptionemMAX_ELEMENT_COUNT = 50_000. Divida relatórios muito grandes em várias chamadas dewriteHtml()ou em vários documentos. - O limite de aninhamento é rígido. Profundidade acima de
MAX_NESTING_DEPTH = 100lanç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 experimentalcss.hasestá 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(). OTableParserlimita 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.
Desempenho
Seção intitulada “Desempenho”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.
Notas de segurança
Seção intitulada “Notas de segurança”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.
Conformidade
Seção intitulada “Conformidade”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.
Contexto comercial
Seção intitulada “Contexto comercial”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.