Ограничения однопроходной потоковой отрисовки HTML (ADR-001)
NextPDF отрисовывает HTML за один проход вперёд и не хранит дерево элементов в памяти. ADR-001 фиксирует этот выбор и ограничения, которые он накладывает на каждую функцию CSS.
Установка
Заголовок раздела «Установка»composer require nextpdf/core:^3Концептуальный обзор
Заголовок раздела «Концептуальный обзор»Подсистема HTML отрисовывает HTML и CSS в PDF за один потоковый проход. ADR-001 (“Stream-based Rendering Pipeline Retention”, принято 2026-04-06) — архитектурное решение, задающее эту модель. На этой странице описаны сама модель, её границы и ограничения, которые должны соблюдать участники проекта.
В этой модели токенизатор (HtmlTokenizer) читает входные данные один раз и формирует плоский список токенов. HtmlParser::processTokens() проходит по этому списку слева направо и записывает операторы потока содержимого PDF в строковый буфер по мере достижения каждого элемента. Между вызовами движок не строит постоянный граф элементов. Любое состояние, которое должно сохраниться после вызова обработчика, передаётся через объект-значение со снимком (HtmlBlockCursor), а не через общие узлы. Наследование стилей построено на стеке: в него помещаются и из него извлекаются плоские экземпляры HtmlStyleState, а не дерево со ссылками на родителя.
Это не модель с удержанием всего документа. Движок не удерживает дерево документа, не перекомпоновывает уже записанное содержимое и не допускает изменения входных данных после начала разбора. Правило простое: NextPDF работает в потоковом режиме от начала до конца. Отрисовщик с удержанием сначала строит весь документ в памяти; NextPDF этого не делает.
Для двух операций нужен ограниченный просмотр вперёд. Обе они — явные ограниченные исключения. Расчёт ширины столбцов таблицы просматривает каждую строку перед размещением ячеек. Для этого строки буферизуются в эфемерном буфере таблицы внутри TableParser — это исключение, которое ADR-001 признаёт явно. Реляционный селектор :has(), а также селекторы :last-child и :last-of-type используют ограниченный предварительный просмотр плоского списка токенов, а не обход дерева. ADR-001 фиксирует оба этих исключения и их пределы.
Модель безопасна при повторном использовании воркера. HtmlParser создаётся один раз на каждый запрос, но никогда как одиночка. HtmlParser::parse() сбрасывает каждое поле в начале каждого вызова. В пути отрисовки нет статического изменяемого состояния, поэтому RoadRunner, Swoole и Laravel Octane могут повторно использовать процесс без утечки состояния между документами.
Поверхность API
Заголовок раздела «Поверхность API»Ниже перечислены символы, которые обеспечивают соблюдение этих ограничений. Сверяйте каждый из них с src/Html/.
| Символ | Расположение | Роль |
|---|---|---|
HtmlParser::parse(string $html): HtmlRenderResult | src/Html/HtmlParser.php | Точка входа. Сбрасывает всё состояние и выполняет один проход. |
HtmlParser::MAX_ELEMENT_COUNT (50_000) | src/Html/HtmlParser.php | Жёсткий предел числа обрабатываемых элементов. |
HtmlParser::MAX_NESTING_DEPTH (100) | src/Html/HtmlParser.php | Жёсткий предел глубины вложенности. |
HtmlBlockCursor | src/Html/HtmlBlockCursor.php | Снимок курсора. Единственный механизм разделяемого состояния. |
HtmlStyleState | src/Html/HtmlStyleState.php | Кадр стиля в стеке. Без указателя на родителя. |
TableParser::reset() | src/Html/TableParser.php | Обязательный сброс эфемерного буфера таблицы между таблицами. |
Пример кода — быстрый старт
Заголовок раздела «Пример кода — быстрый старт»Вы не управляете потоковой моделью напрямую. Один вызов отрисовывает любой поддерживаемый документ.
<?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');Пример кода — рабочая среда
Заголовок раздела «Пример кода — рабочая среда»Отрисуйте большой документ в пределах фиксированного бюджета памяти. Предел числа элементов — защитная граница, поэтому оцените размер входных данных перед вызовом.
<?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);}Крайние случаи и подводные камни
Заголовок раздела «Крайние случаи и подводные камни»- Предел числа элементов — жёсткая остановка. Движок выбрасывает
HtmlParsingExceptionприMAX_ELEMENT_COUNT = 50_000. Разбивайте очень большие отчёты на несколько вызововwriteHtml()или на несколько документов. - Предел вложенности — жёсткая остановка. Глубина выше
MAX_NESTING_DEPTH = 100приводит к исключению. Обычно причина — глубоко вложенные обёртки. - Предел размера входных данных.
HtmlParser::parse()отклоняет входные данные больше 10 МБ до токенизации. :has()доступен по флагу. Предварительный просмотр для:has()выполняется только тогда, когда активна экспериментальная возможностьcss.has. Без неё селекторы:has()не срабатывают.- Буферизация таблицы — единственное эфемерное дерево. Одна очень широкая или очень высокая таблица удерживает свои строки в памяти до
render().TableParserограничивает этот буфер пределами каждой таблицы и сбрасывает его между таблицами; это не дерево уровня всего документа. - Без перекомпоновки. Уже записанное содержимое никогда не перемещается. Стиль, появившийся позже, не может задним числом изменить ранее сформированный вывод.
Производительность
Заголовок раздела «Производительность»Потоковая модель удерживает не более одного HtmlStyleState на каждый уровень вложенности, ограниченный значением MAX_NESTING_DEPTH = 100, плюс активные поля курсора. Память на состояние стилей и курсор составляет O(depth), а не O(element count). ADR-001 фиксирует проектное намерение: этот объём должен оставаться значительно ниже, чем у удерживаемого графа объектов для тех же входных данных. Контролируемый эталонный тест пикового размера резидентного набора (RSS) на 50,000 элементов — цель эмпирической проверки, названная в ADR-001. Эталонный тест производительности конвейера отрисовки HTML отслеживает её с порогом регрессии 5% (работа внедрена в PR #564). Рассматривайте постраничный performance_budget (wall_ms: 1500, peak_mb: 64) как эксплуатационный потолок.
Замечания по безопасности
Заголовок раздела «Замечания по безопасности»Описанные на этой странице пределы также работают как защита от отказа в обслуживании. DefaultHtmlSecurityPolicy обеспечивает соблюдение потолка входных данных в 10 МБ и потолка вложенности в 100 уровней независимо от парсера, поэтому вредоносный документ не может исчерпать память за счёт глубины или размера. Сама потоковая модель ограничивает память по своей конструкции: нет графа элементов, который злоумышленник мог бы раздуть. Полный охват политики смотрите в модели безопасности модуля HTML и в контрактах слоёв.
Соответствие
Заголовок раздела «Соответствие»Эта страница не ссылается ни на один внешний стандарт. Эти ограничения исходят из ADR-001 и из символов исходного кода, которые их обеспечивают и перечислены в разделе “Поверхность API”. Сопоставления поведения со спецификацией CSS документированы на странице css-resolver, а не здесь.
Коммерческий контекст
Заголовок раздела «Коммерческий контекст»Корпоративная возможность. Потоковая архитектура идентична в Core и Premium. Premium расширяет покрытие CSS; он не изменяет однопроходную модель и не ослабляет эти пределы. Смотрите матрицу поддержки CSS.