Vincoli dello streaming HTML a passata unica (ADR-001)
In breve
Sezione intitolata “In breve”NextPDF esegue il rendering dell’HTML in un’unica passata in avanti, senza mantenere in memoria alcun albero degli elementi. L’ADR-001 documenta questa decisione e i vincoli che impone a ogni funzionalità CSS.
Installazione
Sezione intitolata “Installazione”composer require nextpdf/core:^3Panoramica concettuale
Sezione intitolata “Panoramica concettuale”Il sottosistema HTML è un renderer di streaming a passata unica da HTML+CSS a PDF. L’ADR-001 («Stream-based Rendering Pipeline Retention», approvato il 2026-04-06) è la decisione architetturale che definisce questo modello. Questa pagina spiega che cos’è il modello, che cosa non fa e quali vincoli impone ai contributori.
Nel modello di streaming, il tokenizer (HtmlTokenizer) legge l’input una sola volta e produce un elenco piatto di token. HtmlParser::processTokens() percorre tale elenco da sinistra a destra. Man mano che raggiunge ciascun elemento, scrive gli operatori del content stream PDF in un buffer di stringa. Il motore non costruisce alcun grafo di elementi persistente tra le chiamate. Lo stato che deve sopravvivere all’invocazione di un handler passa attraverso un value object di snapshot (HtmlBlockCursor), non attraverso nodi condivisi. L’ereditarietà degli stili usa uno stack di istanze piatte di HtmlStyleState, gestito con push-and-pop, non un albero con puntatori al genitore.
Questo non è un modello a documento trattenuto. Il motore non mantiene un albero del documento, non ridispone il contenuto già scritto e non consente di mutare l’input dopo l’avvio del parsing. Il confine è netto: NextPDF opera in streaming dall’inizio alla fine. Un renderer a documento trattenuto costruisce prima l’intero documento in memoria; NextPDF no.
Due operazioni richiedono un lookahead limitato e costituiscono eccezioni esplicite e circoscritte. Il dimensionamento delle colonne di una tabella esamina ogni riga prima di posizionare una cella. Queste righe vengono mantenute in un buffer di tabella effimero all’interno di TableParser, un’eccezione che l’ADR-001 riconosce esplicitamente. Il selettore relazionale :has() e i selettori :last-child e :last-of-type utilizzano una pre-scansione delimitata sull’elenco piatto di token, non un attraversamento dell’albero. L’ADR-001 documenta entrambe le eccezioni e ne delimita l’ambito.
Il modello è sicuro per l’uso con i worker. HtmlParser viene costruito una volta per richiesta, mai come singleton. HtmlParser::parse() reimposta tutti i campi all’inizio di ogni chiamata. Nel percorso di rendering non esiste alcuno stato statico mutabile, perciò RoadRunner, Swoole e Laravel Octane riutilizzano il processo senza che lo stato trapeli tra un documento e l’altro.
Superficie API
Sezione intitolata “Superficie API”I simboli seguenti applicano i vincoli descritti qui sotto. Verificare ciascuno in src/Html/.
| Simbolo | Posizione | Ruolo |
|---|---|---|
HtmlParser::parse(string $html): HtmlRenderResult | src/Html/HtmlParser.php | Punto di ingresso. Reimposta tutto lo stato, poi esegue la passata unica. |
HtmlParser::MAX_ELEMENT_COUNT (50_000) | src/Html/HtmlParser.php | Limite rigido sul numero di elementi elaborati. |
HtmlParser::MAX_NESTING_DEPTH (100) | src/Html/HtmlParser.php | Limite rigido sulla profondità di annidamento. |
HtmlBlockCursor | src/Html/HtmlBlockCursor.php | Snapshot del cursore. L’unico meccanismo per lo stato condiviso. |
HtmlStyleState | src/Html/HtmlStyleState.php | Frame di stile inserito nello stack. Nessun puntatore al genitore. |
TableParser::reset() | src/Html/TableParser.php | Reimpostazione obbligatoria del buffer di tabella effimero tra le tabelle. |
Esempio di codice — Avvio rapido
Sezione intitolata “Esempio di codice — Avvio rapido”Il modello di streaming è invisibile ai chiamanti. Una singola chiamata esegue il rendering di qualsiasi documento supportato.
<?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');Esempio di codice — Produzione
Sezione intitolata “Esempio di codice — Produzione”Eseguire il rendering di un documento di grandi dimensioni entro un budget di memoria fisso. Il limite sugli elementi è il confine di sicurezza; dimensionare l’input prima della chiamata.
<?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);}Casi limite e insidie
Sezione intitolata “Casi limite e insidie”- Il limite sugli elementi è un arresto netto. Il motore lancia
HtmlParsingExceptionaMAX_ELEMENT_COUNT = 50_000. Suddividere i report molto grandi in più chiamate awriteHtml()o in più documenti. - Il limite sull’annidamento è un arresto netto. Una profondità superiore a
MAX_NESTING_DEPTH = 100genera un’eccezione. I wrapper profondamente annidati ne sono la causa abituale. - La dimensione dell’input ha un limite.
HtmlParser::parse()rifiuta l’input superiore a 10 MB prima della tokenizzazione. :has()è soggetto a un gate. La pre-scansione di:has()viene eseguita solo quando la funzionalità sperimentalecss.hasè attiva. Senza questa abilitazione, i selettori:has()non corrispondono.- La bufferizzazione delle tabelle è l’unico albero effimero. Una singola tabella molto larga o molto alta mantiene le proprie righe in memoria fino a
render().TableParserdelimita questo buffer per ogni tabella e lo reimposta tra una tabella e l’altra; non è un albero a livello dell’intero documento. - Nessuna ridisposizione del contenuto. Il contenuto già scritto non viene mai spostato. Uno stile applicato tardivamente non può modificare retroattivamente l’output precedente.
Prestazioni
Sezione intitolata “Prestazioni”Il modello di streaming mantiene al massimo un HtmlStyleState per livello di annidamento, limitato da MAX_NESTING_DEPTH = 100, oltre ai campi del cursore attivo. La memoria utilizzata da stato di stile e cursore è O(depth), non O(element count). L’ADR-001 documenta l’intento progettuale: questo valore deve rimanere nettamente al di sotto di un grafo di oggetti trattenuto per lo stesso input. Il benchmark controllato del picco RSS a 50,000 elementi è l’obiettivo di validazione empirica indicato nell’ADR-001. Viene monitorato tramite il benchmark prestazionale della pipeline di rendering HTML e il relativo gate di regressione del 5% (lavoro già integrato, PR #564). Considerare il performance_budget per pagina (wall_ms: 1500, peak_mb: 64) come tetto operativo.
Note sulla sicurezza
Sezione intitolata “Note sulla sicurezza”I limiti descritti in questa pagina fungono anche da controlli contro gli attacchi denial-of-service. DefaultHtmlSecurityPolicy applica un tetto di 10 MB all’input e il tetto di 100 livelli di annidamento in modo indipendente dal parser, perciò un documento ostile non può esaurire la memoria tramite profondità o dimensioni. Il modello di streaming delimita già la memoria per costruzione: non esiste alcun grafo di elementi che un utente malintenzionato possa gonfiare. Per l’intera superficie delle policy, vedere il modello di sicurezza del modulo HTML e i contratti di livello.
Conformità
Sezione intitolata “Conformità”Questa pagina non cita standard esterni. I vincoli derivano dall’ADR-001 e dai simboli del sorgente che li applicano, elencati nella sezione Superficie API. Le mappature del comportamento rispetto alla specifica CSS sono documentate in css-resolver, non qui.
Contesto commerciale
Sezione intitolata “Contesto commerciale”Funzionalità Enterprise. L’architettura di streaming è identica in Core e Premium. Premium amplia la copertura CSS; non modifica il modello a passata unica né allenta questi limiti. Vedere la matrice di supporto CSS.