Zum Inhalt springen

HTML-Streaming-Beschränkungen im Single-Pass (ADR-001)

NextPDF rendert HTML in einem einzigen Vorwärtsdurchlauf und hält keinen Elementbaum im Speicher. ADR-001 dokumentiert diese Entscheidung sowie die Beschränkungen, die sie jedem CSS-Feature auferlegt.

Terminal-Fenster
composer require nextpdf/core:^3

Das HTML-Subsystem ist ein Single-Pass-Streaming-Renderer, der HTML+CSS in PDF umwandelt. ADR-001 („Stream-based Rendering Pipeline Retention“, angenommen am 2026-04-06) ist die Architekturentscheidung, die dieses Modell festschreibt. Diese Seite erklärt, was das Modell ist, was es nicht tut und welche Beschränkungen es Mitwirkenden auferlegt.

Im Streaming-Modell liest der Tokenizer (HtmlTokenizer) die Eingabe einmal und erzeugt eine flache Token-Liste. HtmlParser::processTokens() durchläuft diese Liste von links nach rechts und schreibt PDF-Content-Stream-Operatoren in einen String-Puffer, sobald es ein Element erreicht. Die Engine baut zwischen den Aufrufen keinen dauerhaften Elementgraphen auf. Zustand, der über einen Handler-Aufruf hinaus bestehen muss, wird über ein Snapshot-Value-Object (HtmlBlockCursor) geführt, nicht über gemeinsam genutzte Knoten. Die Stilvererbung arbeitet mit einem Push-and-Pop-Stack aus flachen HtmlStyleState-Instanzen, nicht mit einem Baum mit Parent-Zeigern.

Es handelt sich nicht um ein Retained-Document-Modell. Die Engine hält keinen Dokumentbaum vor, berechnet das Layout bereits geschriebener Inhalte nicht erneut und erlaubt keine Änderungen an der Eingabe, nachdem das Parsen begonnen hat. Die Grenze ist klar: NextPDF streamt von Anfang bis Ende. Ein Retained-Renderer baut zuerst das gesamte Dokument im Speicher auf; NextPDF tut das nicht.

Zwei Operationen benötigen begrenztes Lookahead und sind explizite, beschränkte Ausnahmen. Die Spaltenbreitenberechnung von Tabellen liest jede Zeile, bevor eine Zelle platziert wird. Diese Zeilen werden in einem flüchtigen Tabellenpuffer innerhalb von TableParser gepuffert — eine Ausnahme, die ADR-001 namentlich anerkennt. Der relationale Selektor :has() und die Selektoren :last-child und :last-of-type nutzen einen beschränkten Vorabscan über die flache Token-Liste, keinen Baumdurchlauf. ADR-001 hält beide Ausnahmen fest und begrenzt sie.

Das Modell ist für Worker sicher. HtmlParser wird einmal pro Request konstruiert, nie als Singleton. HtmlParser::parse() setzt zu Beginn jedes Aufrufs alle Felder zurück. Im Render-Pfad existiert kein statischer, veränderlicher Zustand; daher können RoadRunner, Swoole und Laravel Octane den Prozess wiederverwenden, ohne dass Zustand zwischen Dokumenten durchsickert.

Diese Symbole erzwingen die folgenden Beschränkungen. Prüfen Sie jedes davon anhand von src/Html/.

SymbolSpeicherortRolle
HtmlParser::parse(string $html): HtmlRenderResultsrc/Html/HtmlParser.phpEinstiegspunkt. Setzt den gesamten Zustand zurück und führt dann den einzelnen Durchlauf aus.
HtmlParser::MAX_ELEMENT_COUNT (50_000)src/Html/HtmlParser.phpHarte Obergrenze für verarbeitete Elemente.
HtmlParser::MAX_NESTING_DEPTH (100)src/Html/HtmlParser.phpHarte Obergrenze für die Verschachtelungstiefe.
HtmlBlockCursorsrc/Html/HtmlBlockCursor.phpCursor-Snapshot. Der einzige Mechanismus für gemeinsam genutzten Zustand.
HtmlStyleStatesrc/Html/HtmlStyleState.phpAuf den Stack gepushter Stil-Frame. Kein Parent-Zeiger.
TableParser::reset()src/Html/TableParser.phpVerpflichtendes Zurücksetzen des flüchtigen Tabellenpuffers zwischen Tabellen.

Für Aufrufende bleibt das Streaming-Modell unsichtbar. Ein Aufruf rendert jedes unterstützte Dokument.

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

Rendern Sie ein großes Dokument unter einem festen Speicherbudget. Die Elementobergrenze ist die Sicherheitsgrenze; dimensionieren Sie die Eingabe vor dem Aufruf.

<?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);
}
  • Die Elementobergrenze ist ein hartes Abbruchkriterium. Die Engine wirft HtmlParsingException bei MAX_ELEMENT_COUNT = 50_000. Teilen Sie sehr große Reports in mehrere writeHtml()-Aufrufe oder mehrere Dokumente auf.
  • Die Verschachtelungsobergrenze ist ein hartes Abbruchkriterium. Eine Tiefe über MAX_NESTING_DEPTH = 100 löst eine Ausnahme aus. Tief verschachtelte Wrapper sind die übliche Ursache.
  • Obergrenze für die Eingabegröße. HtmlParser::parse() weist Eingaben über 10 MB noch vor der Tokenisierung ab.
  • :has() ist an ein Feature Gate gebunden. Der :has()-Vorabscan läuft nur, wenn das experimentelle Feature css.has aktiv ist. Ohne dieses Feature matchen :has()-Selektoren nicht.
  • Tabellenpufferung ist der einzige flüchtige Baum. Bei einer einzelnen, sehr breiten oder sehr hohen Tabelle bleiben die Zeilen bis zu render() im Speicher. TableParser begrenzt diesen Puffer pro Tabelle und setzt ihn zwischen Tabellen zurück; er ist kein dokumentweiter Baum.
  • Kein erneutes Layout. Bereits geschriebene Inhalte werden nie verschoben. Ein späterer Stil kann frühere Ausgaben nicht nachträglich ändern.

Das Streaming-Modell hält höchstens einen HtmlStyleState pro Verschachtelungsebene, begrenzt durch MAX_NESTING_DEPTH = 100, plus die aktiven Cursor-Felder. Der Speicher für Stilzustand und Cursor ist O(Tiefe), nicht O(Elementanzahl). ADR-001 dokumentiert die Designabsicht, dass dieser Speicherbedarf für dieselbe Eingabe deutlich unter dem eines im Speicher gehaltenen Objektgraphen bleibt. Das kontrollierte Peak-RSS-Benchmark mit 50,000 Elementen ist das in ADR-001 benannte empirische Validierungsziel. Dieses Ziel wird über das Performance-Benchmark der HTML-Render-Pipeline und sein 5-%-Regressions-Gate verfolgt (abgeschlossene Arbeit, PR #564). Behandeln Sie das Per-Page-performance_budget (wall_ms: 1500, peak_mb: 64) als operative Obergrenze.

Die Obergrenzen auf dieser Seite sind zugleich Denial-of-Service-Schutzmaßnahmen. DefaultHtmlSecurityPolicy erzwingt eine Eingabeobergrenze von 10 MB und die Verschachtelungsobergrenze von 100 Ebenen unabhängig vom Parser, sodass ein feindliches Dokument den Speicher nicht über Tiefe oder Größe erschöpfen kann. Das Streaming-Modell selbst begrenzt den Speicher konstruktionsbedingt: Es gibt keinen Elementgraphen, den ein Angreifer aufblähen könnte. Die vollständige Policy-Oberfläche finden Sie im Sicherheitsmodell des HTML-Moduls und in den Layer-Verträgen.

Diese Seite zitiert keinen externen Standard. Die Beschränkungen leiten sich aus ADR-001 und den durchsetzenden Quellsymbolen ab, die im Abschnitt API-Oberfläche aufgeführt sind. Verhaltensbezogene Zuordnungen zur CSS-Spezifikation sind unter css-resolver dokumentiert, nicht hier.

Enterprise-Fähigkeit. Die Streaming-Architektur ist in Core und Premium identisch. Premium erweitert die CSS-Abdeckung; das Single-Pass-Modell bleibt unverändert, und diese Obergrenzen werden nicht gelockert. Siehe die CSS-Support-Matrix.