Rendering Pipeline

A well-structured rendering pipeline ensures consistent updates and good performance.

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);
    }
  }
}