콘텐츠로 이동

HTML 단일 패스 스트리밍 제약 조건 (ADR-001)

NextPDF는 HTML을 단일 정방향 패스로 렌더링하며 어떠한 요소 트리도 메모리에 보관하지 않습니다. ADR-001은 이 결정과 그 결정이 모든 CSS 기능에 부과하는 제약을 기록합니다.

Terminal window
composer require nextpdf/core:^3

HTML 서브시스템은 단일 패스 스트리밍 HTML+CSS-to-PDF 렌더러입니다. ADR-001(“Stream-based Rendering Pipeline Retention”, 2026-04-06에 승인)은 이 모델을 확정한 아키텍처 결정입니다. 이 페이지는 이 모델이 무엇인지, 무엇을 하지 않는지, 그리고 기여자에게 어떤 제약을 부과하는지 설명합니다.

스트리밍 모델에서 토크나이저(HtmlTokenizer)는 입력을 한 번 읽고 평탄한 토큰 목록을 생성합니다. HtmlParser::processTokens()는 그 목록을 왼쪽에서 오른쪽으로 순회합니다. 각 요소에 도달할 때마다 PDF 콘텐츠 스트림 연산자를 문자열 버퍼에 기록합니다. 엔진은 호출 사이에 지속되는 요소 그래프를 구축하지 않습니다. 핸들러 호출 이후에도 유지해야 하는 상태는 공유 노드 대신 스냅샷 값 객체(HtmlBlockCursor)를 통해 전달됩니다. 스타일 상속은 부모 포인터 트리가 아니라 평탄한 HtmlStyleState 인스턴스의 푸시-팝 스택을 사용합니다.

이는 보존형 문서(retained-document) 모델이 아닙니다. 엔진은 문서 트리를 보유하지 않으며, 이미 기록한 콘텐츠를 다시 레이아웃하지 않고, 파싱이 시작된 후에는 입력이 변경되는 것을 허용하지 않습니다. 경계는 명확합니다. NextPDF는 처음부터 끝까지 스트리밍합니다. 보존형 렌더러는 전체 문서를 먼저 메모리에 구축하지만, NextPDF는 그렇게 하지 않습니다.

두 가지 연산에는 제한적인 룩어헤드가 필요하며, 이는 명시적으로 범위가 한정된 예외입니다. 표 열 크기 조정은 셀을 배치하기 전에 모든 행을 스캔합니다. 이 행들은 TableParser 내부의 임시 표 버퍼에 버퍼링되며, 이는 ADR-001에서 이름을 명시해 인정하는 예외입니다. :has() 관계 선택자와 :last-child:last-of-type 선택자는 트리 순회 대신 평탄한 토큰 목록에 대한 제한된 사전 스캔을 사용합니다. ADR-001은 두 예외를 모두 기록하고 범위를 한정합니다.

이 모델은 워커 안전(worker-safe)합니다. HtmlParser는 요청마다 한 번 생성되며, 싱글턴으로는 생성되지 않습니다. HtmlParser::parse()는 호출될 때마다 시작 시점에 모든 필드를 재설정합니다. 렌더링 경로에는 정적 가변 상태가 존재하지 않으므로, 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스택에 푸시되는 스타일 프레임. 부모 포인터 없음.
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);
}
  • 요소 상한은 하드 스톱입니다. 엔진은 MAX_ELEMENT_COUNT = 50_000에 도달하면 HtmlParsingException을 던집니다. 매우 큰 보고서는 여러 번의 writeHtml() 호출이나 여러 문서로 분할하세요.
  • 중첩 상한은 하드 스톱입니다. MAX_NESTING_DEPTH = 100을 초과하는 깊이는 예외를 발생시킵니다. 깊게 중첩된 래퍼가 일반적인 원인입니다.
  • 입력 크기 상한. HtmlParser::parse()는 토큰화하기 전에 10 MB를 초과하는 입력을 거부합니다.
  • :has()는 게이트로 제어됩니다. :has() 사전 스캔은 css.has 실험적 기능을 활성화한 경우에만 실행됩니다. 활성화하지 않으면 :has() 선택자는 일치하지 않습니다.
  • 표 버퍼링이 유일한 임시 트리입니다. 매우 넓거나 매우 긴 단일 표는 render()까지 해당 행을 메모리에 보유합니다. TableParser는 이 버퍼를 각 표로 한정하고 표 사이에서 재설정합니다. 이는 문서 전체에 걸쳐 유지되는 트리가 아닙니다.
  • 재레이아웃 없음. 이미 기록된 콘텐츠는 절대 이동되지 않습니다. 뒤늦은 스타일은 앞선 출력을 소급하여 변경할 수 없습니다.

스트리밍 모델은 중첩 수준마다 최대 하나의 HtmlStyleState와 활성 커서 필드를 보유하며, 이는 MAX_NESTING_DEPTH = 100으로 한정됩니다. 스타일 상태와 커서 메모리 사용량은 O(요소 수)가 아니라 O(깊이)입니다. ADR-001은 동일한 입력에서 이 값이 보존형 객체 그래프보다 충분히 낮게 유지되어야 한다는 설계 의도를 기록합니다. 통제된 50,000개 요소 피크 RSS 벤치마크가 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 커버리지를 확장합니다. 단일 패스 모델을 변경하거나 이러한 상한을 완화하지는 않습니다. CSS 지원 매트릭스를 참조하세요.