Beperkingen van single-pass streaming voor HTML (ADR-001)
In één oogopslag
Sectie met titel “In één oogopslag”NextPDF rendert HyperText Markup Language (HTML) in één voorwaartse pass en houdt geen elementenboom in het geheugen vast. ADR-001 legt die keuze vast, samen met de beperkingen die dit oplegt aan elke Cascading Style Sheets-functie (CSS).
Installatie
Sectie met titel “Installatie”composer require nextpdf/core:^3Conceptueel overzicht
Sectie met titel “Conceptueel overzicht”Het HTML-subsysteem rendert HTML en CSS in één streaming-pass naar Portable Document Format (PDF). ADR-001 („Stream-based Rendering Pipeline Retention”, aanvaard op 2026-04-06) is de architectuurbeslissing die dit model definieert. Deze pagina legt het model uit, inclusief de grenzen ervan en de beperkingen die bijdragers moeten respecteren.
In dit model leest de tokenizer (HtmlTokenizer) de invoer één keer en produceert hij een platte tokenlijst. HtmlParser::processTokens() loopt die lijst van links naar rechts af en schrijft, zodra elk element wordt bereikt, PDF-content-streamoperatoren naar een stringbuffer. De engine bouwt tussen aanroepen geen persistente elementgraaf op. Elke status die een handleraanroep moet overleven, loopt via een snapshot-value-object (HtmlBlockCursor), niet via gedeelde nodes. Stijlovererving gebruikt een push-and-pop-stack met platte HtmlStyleState-instanties, geen boom met verwijzingen naar ouders.
Dit is geen model met een vastgehouden document. De engine houdt geen documentboom vast, plaatst reeds geschreven content niet opnieuw en laat de invoer niet wijzigen nadat de parse is gestart. De grens is duidelijk: NextPDF streamt van begin tot eind. Een renderer met vastgehouden documenten bouwt eerst het volledige document in het geheugen op; NextPDF doet dat niet.
Twee bewerkingen vereisen beperkte lookahead. Beide zijn expliciete, begrensde uitzonderingen. Voor het bepalen van de kolombreedte van een tabel wordt elke rij gescand voordat een cel wordt geplaatst. Die rijen worden gebufferd in een kortstondige tabelbuffer binnen TableParser, een uitzondering die ADR-001 expliciet erkent. De relationele selector :has() en de selectoren :last-child en :last-of-type gebruiken een begrensde pre-scan over de platte tokenlijst, geen boomdoorloop. ADR-001 legt beide uitzonderingen en hun grenzen vast.
Het model is worker-veilig. HtmlParser wordt één keer per request geconstrueerd, nooit als singleton. HtmlParser::parse() reset elk veld aan het begin van elke aanroep. In het renderpad bestaat geen statische, muteerbare status, zodat RoadRunner, Swoole en Laravel Octane het proces kunnen hergebruiken zonder dat status tussen documenten lekt.
API-oppervlak
Sectie met titel “API-oppervlak”De onderstaande symbolen dwingen deze beperkingen af. Controleer elk ervan aan de hand van src/Html/.
| Symbool | Locatie | Rol |
|---|---|---|
HtmlParser::parse(string $html): HtmlRenderResult | src/Html/HtmlParser.php | Toegangspunt. Reset alle status en voert daarna de single pass uit. |
HtmlParser::MAX_ELEMENT_COUNT (50_000) | src/Html/HtmlParser.php | Harde limiet op verwerkte elementen. |
HtmlParser::MAX_NESTING_DEPTH (100) | src/Html/HtmlParser.php | Harde limiet op nestingdiepte. |
HtmlBlockCursor | src/Html/HtmlBlockCursor.php | Cursorsnapshot. Het enige mechanisme voor gedeelde status. |
HtmlStyleState | src/Html/HtmlStyleState.php | Stijlframe op de stack. Geen verwijzing naar een ouder. |
TableParser::reset() | src/Html/TableParser.php | Verplichte reset van de kortstondige tabelbuffer tussen tabellen. |
Codevoorbeeld — Snelstart
Sectie met titel “Codevoorbeeld — Snelstart”Je beheert het streamingmodel niet rechtstreeks. Eén aanroep rendert elk ondersteund document.
<?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');Codevoorbeeld — Productie
Sectie met titel “Codevoorbeeld — Productie”Rendert een groot document binnen een vast geheugenbudget. De elementlimiet is de veiligheidsgrens, dus bepaal de omvang van de invoer voordat je de aanroep doet.
<?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);}Randgevallen en valkuilen
Sectie met titel “Randgevallen en valkuilen”- Elementlimiet is een harde stop. De engine gooit
HtmlParsingExceptionbijMAX_ELEMENT_COUNT = 50_000. Splits zeer grote rapporten op over meerderewriteHtml()-aanroepen of meerdere documenten. - Nestinglimiet is een harde stop. Een diepte boven
MAX_NESTING_DEPTH = 100gooit een exception. Diep geneste wrappers zijn hier meestal de oorzaak van. - Limiet op invoergrootte.
HtmlParser::parse()weigert invoer groter dan 10 MB vóór het tokeniseren. :has()staat achter een gate. De pre-scan van:has()draait alleen wanneer de experimentele functiecss.hasactief is. Zonder deze functie matchen:has()-selectoren niet.- Tabelbuffering is de enige kortstondige boom. Eén enkele zeer brede of zeer hoge tabel houdt zijn rijen in het geheugen tot
render().TableParserbegrenst deze buffer per tabel en reset hem tussen tabellen; het is geen documentbrede boom. - Geen herindeling. Reeds geschreven content wordt nooit verplaatst. Een later toegepaste stijl kan eerdere uitvoer niet met terugwerkende kracht wijzigen.
Prestaties
Sectie met titel “Prestaties”Het streamingmodel houdt maximaal één HtmlStyleState per nestingniveau vast, begrensd door MAX_NESTING_DEPTH = 100, plus de actieve cursorvelden. Het geheugen voor stijlstatus en cursor is O(diepte), niet O(aantal elementen). ADR-001 legt de ontwerpintentie vast dat dit ruim onder een vastgehouden objectgraaf voor dezelfde invoer blijft. De gecontroleerde benchmark voor de piek-resident-set-size (RSS) bij 50,000 elementen is het empirische validatiedoel dat in ADR-001 wordt genoemd. De prestatiebenchmark van de HTML-renderpipeline volgt dit met een regressiegate van 5% (samengevoegd werk, PR #564). Beschouw het performance_budget per pagina (wall_ms: 1500, peak_mb: 64) als het operationele plafond.
Beveiligingsnotities
Sectie met titel “Beveiligingsnotities”De limieten op deze pagina fungeren ook als denial-of-service-controles. DefaultHtmlSecurityPolicy handhaaft onafhankelijk van de parser een invoerplafond van 10 MB en een nestingplafond van 100 niveaus, zodat een vijandig document het geheugen niet via diepte of grootte kan uitputten. Het streamingmodel begrenst het geheugen al door zijn opzet: er is geen elementgraaf die een aanvaller kan opblazen. Raadpleeg het beveiligingsmodel van de HTML-module en de laagcontracten voor het volledige beleidsoppervlak.
Conformiteit
Sectie met titel “Conformiteit”Deze pagina citeert geen externe standaard. Deze beperkingen komen voort uit ADR-001 en uit de afdwingende bronsymbolen die onder API-oppervlak worden vermeld. Gedragsmatige mappings naar de CSS-specificatie zijn gedocumenteerd op css-resolver, niet hier.
Commerciële context
Sectie met titel “Commerciële context”Enterprise-mogelijkheid. De streamingarchitectuur is identiek in Core en Premium. Premium verbreedt de CSS-dekking; het wijzigt het single-pass-model niet en versoepelt deze limieten niet. Zie de CSS-ondersteuningsmatrix.