ข้ามไปยังเนื้อหา

ข้อจำกัดของการสตรีมแบบ single-pass สำหรับ HTML (ADR-001)

NextPDF เรนเดอร์ HyperText Markup Language (HTML) ด้วยการประมวลผลไปข้างหน้าเพียงรอบเดียว และไม่เก็บโครงสร้างต้นไม้ของเอลิเมนต์ไว้ในหน่วยความจำ ADR-001 บันทึกการตัดสินใจนี้และข้อจำกัดที่ใช้กับคุณสมบัติของ Cascading Style Sheets (CSS) ทั้งหมด

Terminal window
composer require nextpdf/core:^3

ระบบย่อย HTML เรนเดอร์ HTML และ CSS เป็น Portable Document Format (PDF) ด้วยการสตรีมเพียงรอบเดียว ADR-001 (“Stream-based Rendering Pipeline Retention” ซึ่งยอมรับเมื่อ 2026-04-06) เป็นการตัดสินใจเชิงสถาปัตยกรรมที่กำหนดโมเดลนี้ หน้านี้อธิบายโมเดลดังกล่าว ขอบเขตของโมเดล และข้อจำกัดที่ผู้ร่วมพัฒนาต้องปฏิบัติตาม

ในโมเดลนี้ ตัวแยกโทเค็น (HtmlTokenizer) อ่านอินพุตเพียงครั้งเดียวและสร้างรายการโทเค็นแบบแบน HtmlParser::processTokens() ไล่ผ่านรายการนั้นจากซ้ายไปขวา แล้วเขียนตัวดำเนินการของคอนเทนต์สตรีม PDF ลงในบัฟเฟอร์สตริงเมื่อถึงแต่ละเอลิเมนต์ เอนจินไม่สร้างกราฟของเอลิเมนต์ที่คงอยู่ระหว่างการเรียกแต่ละครั้ง สถานะใดก็ตามที่ต้องคงอยู่ข้ามการเรียกตัวจัดการจะส่งผ่านออบเจ็กต์ค่าแบบสแนปช็อต (HtmlBlockCursor) ไม่ใช่ผ่านโหนดที่ใช้ร่วมกัน การสืบทอดสไตล์ใช้สแตกแบบ push-and-pop ของอินสแตนซ์ HtmlStyleState แบบแบน ไม่ใช่โครงสร้างต้นไม้ที่มีตัวชี้ไปยังโหนดแม่

นี่ไม่ใช่โมเดลแบบคงเอกสารไว้ เอนจินไม่เก็บโครงสร้างต้นไม้ของเอกสาร ไม่จัดวางคอนเทนต์ที่เขียนไปแล้วใหม่ และไม่ยอมให้อินพุตเปลี่ยนแปลงหลังจากเริ่มแยกวิเคราะห์แล้ว ขอบเขตชัดเจน: NextPDF สตรีมตั้งแต่ต้นจนจบ ตัวเรนเดอร์แบบคงเอกสารไว้จะสร้างเอกสารทั้งฉบับในหน่วยความจำก่อน แต่ NextPDF ไม่ทำเช่นนั้น

การดำเนินการสองอย่างต้องใช้การมองล่วงหน้าแบบจำกัด ทั้งสองอย่างเป็นข้อยกเว้นที่ระบุไว้อย่างชัดเจนและมีขอบเขตจำกัด การกำหนดขนาดคอลัมน์ของตารางจะสแกนทุกแถวก่อนวางเซลล์ โดยบัฟเฟอร์แถวเหล่านั้นไว้ในบัฟเฟอร์ตารางแบบชั่วคราวภายใน TableParser ซึ่งเป็นข้อยกเว้นที่ ADR-001 ระบุชื่อไว้ ตัวเลือกเชิงสัมพันธ์ :has() และตัวเลือก :last-child และ :last-of-type ใช้การสแกนล่วงหน้าแบบมีขอบเขตบนรายการโทเค็นแบบแบน ไม่ใช่การไล่ผ่านโครงสร้างต้นไม้ ADR-001 บันทึกข้อยกเว้นทั้งสองอย่างและขอบเขตของข้อยกเว้นเหล่านั้น

โมเดลนี้ปลอดภัยสำหรับการใช้งานแบบ worker HtmlParser ถูกสร้างหนึ่งครั้งต่อหนึ่งคำขอ ไม่ใช่แบบ singleton HtmlParser::parse() รีเซ็ตทุกฟิลด์เมื่อเริ่มต้นการเรียกแต่ละครั้ง ไม่มีสถานะแบบ static ที่เปลี่ยนแปลงได้ในเส้นทางการเรนเดอร์ ดังนั้น RoadRunner, Swoole และ Laravel Octane จึงใช้กระบวนการซ้ำได้โดยไม่มีสถานะรั่วไหลข้ามเอกสาร

สัญลักษณ์ด้านล่างบังคับใช้ข้อจำกัดเหล่านี้ ให้ตรวจสอบแต่ละรายการเทียบกับ src/Html/

สัญลักษณ์ตำแหน่งบทบาท
HtmlParser::parse(string $html): HtmlRenderResultsrc/Html/HtmlParser.phpจุดเริ่มต้น รีเซ็ตสถานะทั้งหมด แล้วจึงเรียกใช้การประมวลผลรอบเดียว
HtmlParser::MAX_ELEMENT_COUNT (50_000)src/Html/HtmlParser.phpเพดานตายตัวของจำนวนเอลิเมนต์ที่ประมวลผล
HtmlParser::MAX_NESTING_DEPTH (100)src/Html/HtmlParser.phpเพดานตายตัวของระดับการซ้อน
HtmlBlockCursorsrc/Html/HtmlBlockCursor.phpสแนปช็อตของเคอร์เซอร์ กลไกสถานะแบบใช้ร่วมกันเพียงอย่างเดียว
HtmlStyleStatesrc/Html/HtmlStyleState.phpเฟรมสไตล์ที่ push เข้าสแตก ไม่มีตัวชี้ไปยังโหนดแม่
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 MB ก่อนการแยกโทเค็น
  • :has() ถูกควบคุมด้วยเกต การสแกนล่วงหน้าของ :has() จะทำงานเฉพาะเมื่อเปิดใช้คุณสมบัติทดลอง css.has เท่านั้น หากไม่เปิดใช้ ตัวเลือก :has() จะไม่จับคู่
  • การบัฟเฟอร์ตารางเป็นโครงสร้างต้นไม้แบบชั่วคราวเพียงหนึ่งเดียว ตารางเดี่ยวที่กว้างมากหรือสูงมากจะเก็บแถวของตารางไว้ในหน่วยความจำจนกระทั่งถึง render() TableParser จำกัดขอบเขตของบัฟเฟอร์นี้ต่อตารางและรีเซ็ตบัฟเฟอร์ระหว่างตาราง บัฟเฟอร์นี้ไม่ใช่โครงสร้างต้นไม้ระดับทั้งเอกสาร
  • ไม่มีการจัดวางใหม่ คอนเทนต์ที่เขียนไปแล้วจะไม่ถูกย้ายไปที่อื่น สไตล์ที่มาภายหลังไม่สามารถย้อนกลับไปเปลี่ยนเอาต์พุตก่อนหน้าได้

โมเดลการสตรีมเก็บ HtmlStyleState ได้มากที่สุดหนึ่งรายการต่อระดับการซ้อน โดยมีขอบเขตจำกัดที่ MAX_NESTING_DEPTH = 100 รวมกับฟิลด์ของเคอร์เซอร์ที่ใช้งานอยู่ หน่วยความจำของสถานะสไตล์และเคอร์เซอร์เป็น O(depth) ไม่ใช่ O(element count) ADR-001 บันทึกเจตนาการออกแบบว่าค่านี้ยังต่ำกว่ากราฟของออบเจ็กต์แบบคงไว้สำหรับอินพุตเดียวกันอย่างมาก การวัดประสิทธิภาพแบบควบคุมของ peak resident set size (RSS) ที่ 50,000 เอลิเมนต์คือเป้าหมายการตรวจสอบเชิงประจักษ์ที่ระบุไว้ใน ADR-001 การวัดประสิทธิภาพของไปป์ไลน์การเรนเดอร์ HTML ติดตามค่านี้ด้วยเกตการถดถอย 5% (งานที่ผสานแล้ว PR #564) ให้ถือว่า performance_budget ต่อหน้า (wall_ms: 1500, peak_mb: 64) เป็นเพดานสำหรับการใช้งานจริง

เพดานต่าง ๆ ในหน้านี้ยังทำหน้าที่เป็นการควบคุมการโจมตีแบบ denial-of-service ด้วย DefaultHtmlSecurityPolicy บังคับใช้เพดานอินพุต 10 MB และเพดานการซ้อน 100 ระดับโดยเป็นอิสระจากตัวแยกวิเคราะห์ ดังนั้นเอกสารที่มุ่งร้ายจึงไม่สามารถทำให้หน่วยความจำหมดผ่านความลึกหรือขนาดได้ โมเดลการสตรีมจำกัดขอบเขตหน่วยความจำตั้งแต่การออกแบบ: ไม่มีกราฟของเอลิเมนต์ให้ผู้โจมตีทำให้พองตัวได้ ดู โมเดลความปลอดภัยของโมดูล HTML และ สัญญาของเลเยอร์ เพื่อดูพื้นผิวของนโยบายทั้งหมด

หน้านี้ไม่ได้อ้างอิงมาตรฐานภายนอกใด ๆ ข้อจำกัดเหล่านี้มาจาก ADR-001 และจากสัญลักษณ์ในซอร์สที่บังคับใช้ซึ่งระบุไว้ภายใต้พื้นผิว API การแมปกับสเปก CSS เชิงพฤติกรรมบันทึกไว้ที่ css-resolver ไม่ใช่ที่นี่

ความสามารถระดับองค์กร สถาปัตยกรรมการสตรีมเหมือนกันทั้งใน Core และ Premium Premium ขยายขอบเขตการรองรับ CSS ให้กว้างขึ้น แต่ไม่ได้เปลี่ยนโมเดลแบบ single-pass หรือผ่อนปรนเพดานเหล่านี้ ดู เมทริกซ์การรองรับ CSS