HTML 單趟串流限制(ADR-001)
NextPDF 以單趟向前掃描繪製 HTML,不在記憶體中保留任何元素樹。 ADR-001 記錄這項決策,以及它對每一項 CSS 功能所施加的限制。
composer require nextpdf/core:^3概念總覽
標題為「概念總覽」的區段HTML 子系統是一個單趟串流的 HTML+CSS 轉 PDF renderer(渲染器)。 ADR-001(標題為「Stream-based Rendering Pipeline Retention」,於 2026-04-06 通過)是確立此模型的架構決策。 本頁說明這個模型是什麼、不做哪些事,以及它對貢獻者施加哪些限制。
在串流模型中,tokenizer(HtmlTokenizer)只讀取輸入一次,並產生一份扁平的 token 清單。 HtmlParser::processTokens() 由左至右逐一走過這份清單。 每遇到一個元素,就把 PDF content-stream 運算子寫進字串緩衝區。 引擎不會在多次呼叫之間建立任何持久的元素圖。 必須跨過一次 handler 呼叫而存活的狀態,是透過一個快照值物件(HtmlBlockCursor)傳遞,而非透過共用節點。 樣式繼承透過一個推入與彈出的堆疊處理,堆疊中放的是扁平的 HtmlStyleState 實例,而非一棵帶父指標的樹。
這不是保留式文件模型。 引擎不持有文件樹、不會對已寫出的內容重新排版,也不允許輸入在剖析開始後再變動。 邊界很明確:NextPDF 從頭到尾都採串流。 保留式 renderer 會先把整份文件建在記憶體中;NextPDF 不這麼做。
有兩項操作需要有限的前瞻,它們都是明確且有界的例外。 表格欄寬計算會在放置儲存格之前先掃描每一列。 它會把這些列暫存在 TableParser 內部的短暫表格緩衝區中,這是 ADR-001 明確列名的例外。 :has() 關聯選擇器,以及 :last-child 與 :last-of-type 選擇器,使用對扁平 token 清單的有界預掃描,而非樹走訪。 ADR-001 記錄這兩項例外,並為它們設下界限。
這個模型對 worker 是安全的。 HtmlParser 每個請求建構一次,絕不作為 singleton(單例)。 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 | Cursor 快照。 唯一的共用狀態機制。 |
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()會在 tokenize 之前就拒絕超過 10 MB 的輸入。 :has()受 gate 控管。:has()預掃描只有在css.has這項實驗性功能啟用時才會執行。 若未啟用,:has()選擇器就不會匹配。- 表格緩衝是唯一的短暫樹狀結構。 單一非常寬或非常高的表格,會把它的列保留在記憶體中,直到
render()為止。TableParser會為每個表格限制這個緩衝區的大小,並在表格之間重置;它不是一棵文件層級的樹。 - 不重新排版。 已經寫出的內容絕不會被移動。 後出現的樣式無法回溯改變較早的輸出。
串流模型在每個巢狀層級最多只持有一個 HtmlStyleState,受 MAX_NESTING_DEPTH = 100 所約束,再加上目前作用中的 cursor 欄位。 樣式狀態與 cursor 的記憶體用量是 O(depth),而非 O(element count)。 ADR-001 記錄的設計意圖是:對相同的輸入,這個用量會明顯低於保留式的物件圖。 受控的 50,000 元素 peak-RSS 基準測試,是 ADR-001 指定的實證驗證目標。 它透過 HTML render-pipeline 效能基準測試及其 5% 回歸 gate 追蹤(相關工作已合併,PR #564)。 請把每頁的 performance_budget(wall_ms: 1500、peak_mb: 64)視為運作時的上限。
安全性說明
標題為「安全性說明」的區段本頁的這些上限同時也是阻斷服務(denial-of-service)的防護控制。 DefaultHtmlSecurityPolicy 會在 parser 之外獨立強制執行 10 MB 的輸入上限與 100 層的巢狀上限,因此惡意文件無法藉由深度或大小耗盡記憶體。 串流模型本身在結構上就約束了記憶體:沒有可讓攻擊者灌大的元素圖。 完整的政策範圍請參閱 HTML 模組安全模型與分層契約。
符合性
標題為「符合性」的區段本頁未引用任何外部標準。 這些限制源自 ADR-001,以及 API 介面一節列出的、落實限制的原始碼符號。 CSS 行為層面的規格對應記錄在 css-resolver 一節,而非本頁。
商業情境
標題為「商業情境」的區段企業級能力。 串流架構在 Core 與 Premium 中完全相同。 Premium 擴大 CSS 涵蓋範圍;它並未改變單趟模型,也不會放寬這些上限。 請參閱 CSS 支援對照表。