개요
렌더링 파이프라인은 문서 모델을 일련의 잘 정의된 단계를 통해 DOM 업데이트로 변환합니다. 이를 통해 예측 가능한 동작을 보장하고 각 단계에서 최적화를 가능하게 합니다.
렌더 단계
단계 분석
렌더링을 구별되는 단계로 나눕니다:
class RenderPipeline {
render(document) {
// Phase 1: Prepare
const prepared = this.#prepare(document);
// Phase 2: Transform
const transformed = this.#transform(prepared);
// Phase 3: Validate
const validated = this.#validate(transformed);
// Phase 4: Render
const dom = this.#renderToDOM(validated);
// Phase 5: Update
this.#updateDOM(dom);
// Phase 6: Post-process
this.#postProcess();
}
#prepare(doc) {
// Normalize structure
// Resolve references
// Prepare data structures
return normalized;
}
#transform(doc) {
// Apply transforms from plugins
// Convert model nodes to render nodes
return transformed;
}
#validate(doc) {
// Ensure document is valid
// Fix any issues
return validated;
}
#renderToDOM(doc) {
// Create DOM elements
// Set attributes
// Build tree
return dom;
}
#updateDOM(newDOM) {
// Diff with current DOM
// Apply updates
// Preserve selection
}
#postProcess() {
// Scroll into view
// Update selection
// Trigger events
}
}단계 순서
단계 순서는 정확성을 위해 중요합니다:
- Prepare: 입력을 정규화하기 위해 먼저 실행되어야 함
- Transform: Prepare 이후, Validate 이전에 실행
- Validate: 변환된 문서가 유효한지 확인
- Render: 유효한 문서에서 DOM 생성
- Update: 기존 DOM에 변경 사항 적용
- Post-process: 최종 정리 및 부작용
업데이트 스케줄링
우선순위 스케줄링
블로킹을 피하기 위해 업데이트를 효율적으로 스케줄링합니다:
class UpdateScheduler {
#pendingUpdates = new Set();
#scheduled = false;
scheduleUpdate(priority, update) {
this.#pendingUpdates.add({ update, priority });
if (priority === 'high') {
// Immediate update
this.#flush();
} else {
// Schedule for next frame
this.#schedule();
}
}
#schedule() {
if (this.#scheduled) return;
this.#scheduled = true;
// Use MessageChannel for immediate scheduling
const channel = new MessageChannel();
channel.port1.onmessage = () => {
this.#flush();
this.#scheduled = false;
};
channel.port2.postMessage(null);
}
#flush() {
const updates = Array.from(this.#pendingUpdates);
this.#pendingUpdates.clear();
// Group by priority
const high = updates.filter(u => u.priority === 'high');
const normal = updates.filter(u => u.priority === 'normal');
const low = updates.filter(u => u.priority === 'low');
// Execute in priority order
high.forEach(({ update }) => update());
normal.forEach(({ update }) => update());
low.forEach(({ update }) => update());
}
}
// Usage
scheduler.scheduleUpdate('high', () => {
// Critical update (e.g., selection)
updateSelection();
});
scheduler.scheduleUpdate('normal', () => {
// Normal update (e.g., content)
updateContent();
});
scheduler.scheduleUpdate('low', () => {
// Low priority (e.g., syntax highlighting)
updateSyntaxHighlighting();
});프레임 스케줄링
부드러운 렌더링을 위해 requestAnimationFrame을 사용합니다:
class FrameScheduler {
#pending = [];
#scheduled = false;
schedule(update) {
this.#pending.push(update);
if (!this.#scheduled) {
this.#scheduled = true;
requestAnimationFrame(() => {
this.#flush();
this.#scheduled = false;
});
}
}
#flush() {
const updates = this.#pending;
this.#pending = [];
updates.forEach(update => {
try {
update();
} catch (error) {
console.error('Update failed:', error);
}
});
}
// Schedule with time budget
scheduleWithBudget(update, budget = 5) {
const start = performance.now();
requestAnimationFrame(() => {
update();
const elapsed = performance.now() - start;
if (elapsed > budget) {
console.warn('Update exceeded budget:', elapsed);
}
});
}
}렌더 최적화
성능을 위한 렌더링 최적화:
class OptimizedPipeline extends RenderPipeline {
#cache = new Map();
#dirtyNodes = new Set();
render(document) {
// Only render dirty nodes
const nodesToRender = this.#getDirtyNodes(document);
if (nodesToRender.size === 0) {
// No changes, return cached result
return this.#cache.get(this.#getDocumentKey(document));
}
// Render only changed nodes
const rendered = {};
nodesToRender.forEach(nodeId => {
const node = this.#getNodeById(nodeId);
rendered[nodeId] = this.#renderNode(node);
});
// Merge with cached results
const result = { ...this.#cache.get(this.#getDocumentKey(document)), ...rendered };
// Update cache
this.#cache.set(this.#getDocumentKey(document), result);
this.#dirtyNodes.clear();
return result;
}
markDirty(nodeId) {
this.#dirtyNodes.add(nodeId);
// Mark ancestors as dirty too
this.#markAncestorsDirty(nodeId);
}
#markAncestorsDirty(nodeId) {
let current = nodeId;
while (current) {
this.#dirtyNodes.add(current);
current = this.#getParentId(current);
}
}
}