Aller au contenu

Contraintes du streaming HTML à passe unique (ADR-001)

NextPDF rend le HTML en une seule passe vers l’avant et ne garde aucun arbre d’éléments en mémoire. L’ADR-001 entérine cette décision et les contraintes qu’elle impose à chaque fonctionnalité CSS.

Fenêtre de terminal
composer require nextpdf/core:^3

Le sous-système HTML est un renderer HTML+CSS vers PDF, en streaming et à passe unique. L’ADR-001 (« Stream-based Rendering Pipeline Retention », acceptée le 2026-04-06) est la décision architecturale qui fixe ce modèle. Cette page précise ce qu’est le modèle, ce qu’il ne fait pas et les contraintes qu’il impose aux contributeurs.

Dans le modèle de streaming, le tokeniseur (HtmlTokenizer) lit l’entrée une seule fois et produit une liste plate de tokens. HtmlParser::processTokens() parcourt cette liste de gauche à droite. Il écrit les opérateurs de flux de contenu PDF dans un tampon de chaîne au fur et à mesure qu’il atteint chaque élément. Le moteur ne construit aucun graphe d’éléments persistant entre les appels. L’état qui doit survivre à un appel de gestionnaire passe par un objet-valeur de capture instantanée (HtmlBlockCursor), et non par des nœuds partagés. L’héritage de styles utilise une pile d’instances HtmlStyleState empilées puis dépilées, et non un arbre à pointeurs parents.

Ce n’est pas un modèle de document conservé. Le moteur ne conserve pas d’arbre de document, ne remet pas en page le contenu déjà écrit et ne laisse pas l’entrée être modifiée une fois l’analyse démarrée. La frontière est claire : NextPDF fait du streaming de bout en bout. Un renderer à document conservé construit d’abord tout le document en mémoire ; NextPDF ne le fait pas.

Deux opérations exigent une anticipation limitée et constituent des exceptions explicites et bornées. Le dimensionnement des colonnes de tableau parcourt chaque ligne avant de placer une cellule. Il met ces lignes en tampon dans un tampon de tableau éphémère à l’intérieur de TableParser, une exception que l’ADR-001 reconnaît explicitement. Le sélecteur relationnel :has() ainsi que les sélecteurs :last-child et :last-of-type utilisent un pré-balayage borné sur la liste plate de tokens, et non un parcours d’arbre. L’ADR-001 entérine ces deux exceptions et les borne.

Le modèle est sûr pour les workers. HtmlParser est instancié une fois par requête, jamais en singleton. HtmlParser::parse() réinitialise chaque champ au début de chaque appel. Aucun état mutable statique n’existe dans le chemin de rendu, ce qui permet à RoadRunner, Swoole et Laravel Octane de réutiliser le processus sans fuite d’état entre les documents.

Ces symboles font appliquer les contraintes ci-dessous. Vérifie chacun face à src/Html/.

SymboleEmplacementRôle
HtmlParser::parse(string $html): HtmlRenderResultsrc/Html/HtmlParser.phpPoint d’entrée. Réinitialise tout l’état, puis exécute la passe unique.
HtmlParser::MAX_ELEMENT_COUNT (50_000)src/Html/HtmlParser.phpPlafond strict du nombre d’éléments traités.
HtmlParser::MAX_NESTING_DEPTH (100)src/Html/HtmlParser.phpPlafond strict de la profondeur d’imbrication.
HtmlBlockCursorsrc/Html/HtmlBlockCursor.phpCapture instantanée du curseur. Le seul mécanisme d’état partagé.
HtmlStyleStatesrc/Html/HtmlStyleState.phpCadre de style empilé. Aucun pointeur parent.
TableParser::reset()src/Html/TableParser.phpRéinitialisation obligatoire du tampon de tableau éphémère entre les tableaux.

Le modèle de streaming est invisible pour les appelants. Un seul appel rend tout document pris en charge.

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

Rends un grand document sous un budget mémoire fixe. Le plafond d’éléments est la limite de sûreté ; dimensionne l’entrée avant l’appel.

<?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);
}
  • Le plafond d’éléments est un arrêt net. Le moteur lève HtmlParsingException à MAX_ELEMENT_COUNT = 50_000. Découpe les très grands rapports en plusieurs appels writeHtml() ou plusieurs documents.
  • Le plafond d’imbrication est un arrêt net. Une profondeur supérieure à MAX_NESTING_DEPTH = 100 lève une exception. Des conteneurs profondément imbriqués en sont la cause habituelle.
  • Plafond de taille de l’entrée. HtmlParser::parse() rejette toute entrée de plus de 10 Mo avant la tokenisation.
  • :has() est conditionnel. Le pré-balayage de :has() ne s’exécute que lorsque la fonctionnalité expérimentale css.has est active. Sans elle, les sélecteurs :has() ne correspondent à rien.
  • La mise en tampon des tableaux est le seul arbre éphémère. Un seul tableau très large ou très haut garde ses lignes en mémoire jusqu’à render(). TableParser borne ce tampon par tableau et le réinitialise entre les tableaux ; ce n’est pas un arbre à l’échelle du document.
  • Aucune remise en page. Le contenu déjà écrit n’est jamais déplacé. Un style tardif ne peut pas modifier rétroactivement une sortie antérieure.

Le modèle de streaming garde au plus un HtmlStyleState par niveau d’imbrication, borné par MAX_NESTING_DEPTH = 100, plus les champs du curseur actif. La mémoire utilisée par l’état de styles et le curseur est en O(profondeur), pas en O(nombre d’éléments). L’ADR-001 entérine l’intention de conception : rester nettement en dessous d’un graphe d’objets conservé pour la même entrée. Le benchmark de pic RSS contrôlé à 50,000 éléments est la cible de validation empirique nommée dans l’ADR-001. Ce suivi passe par le benchmark de performances du pipeline de rendu HTML et son seuil de régression de 5 % (travail fusionné, PR #564). Considère le performance_budget par page (wall_ms: 1500, peak_mb: 64) comme le plafond opérationnel.

Les plafonds décrits sur cette page sont aussi des contrôles contre le déni de service. DefaultHtmlSecurityPolicy impose un plafond d’entrée de 10 Mo et le plafond d’imbrication de 100 niveaux indépendamment de l’analyseur, si bien qu’un document hostile ne peut pas épuiser la mémoire par la profondeur ou la taille. Le modèle de streaming lui-même borne la mémoire par construction : il n’y a aucun graphe d’éléments qu’un attaquant pourrait gonfler. Consulte le modèle de sécurité du module HTML et les contrats de couches pour la surface complète de la politique.

Cette page ne cite aucune norme externe. Les contraintes découlent de l’ADR-001 et des symboles source qui les appliquent, listés sous Surface d’API. Les correspondances comportementales avec les spécifications CSS sont documentées sur css-resolver, pas ici.

Capacité Enterprise. L’architecture de streaming est identique dans Core et dans Premium. Premium élargit la couverture CSS ; il ne change pas le modèle à passe unique et n’assouplit pas ces plafonds. Consulte la matrice de prise en charge CSS.