Overview
The rendering pipeline transforms the document model into DOM updates through a series of well-defined phases. This ensures predictable behavior and enables optimization at each stage.
Render Phases
Phase Breakdown
Break rendering into distinct phases:
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
}
}Phase Ordering
Phase ordering is critical for correctness:
- Prepare: Must run first to normalize input
- Transform: Runs after prepare, before validation
- Validate: Ensures transformed document is valid
- Render: Creates DOM from validated document
- Update: Applies changes to existing DOM
- Post-process: Final cleanup and side effects
Update Scheduling
Priority Scheduling
Schedule updates efficiently to avoid blocking:
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();
});Frame Scheduling
Use requestAnimationFrame for smooth rendering:
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);
}
});
}
}Render Optimization
Optimize rendering for performance:
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);
}
}
}