Javascript is required
Best practicesPublished 2026-07-28Updated 2026-08-088 min read

Large File Diffs: Web Workers and DOM Virtualization

Comparing 10,000-line code files directly on the UI main thread freezes the browser. Even if computation succeeds, rendering tens of thousands of DOM nodes causes frame rates to drop to zero. This article explores engineering solutions.

By: Tiny
Code DiffWeb WorkerVirtualizationPerformance

1. Problem: main thread bottlenecks in CPU computation and massive DOM rendering

Single JavaScript tasks taking over 50ms are categorized as Long Tasks by browsers. Computing diff matrices for 10,000-line files locks the main thread for seconds, blocking UI rendering and user input. Furthermore, constructing tens of thousands of DOM nodes causes heavy Layout and Paint passes.

2. Minimal reproduction: viewport windowing logic for virtual scroll

This logic demonstrates calculating visible row indices (Viewport Windowing) instead of rendering full DOM trees:

interface RenderChunkOptions {

  totalLines: number;
  rowHeight: number;
  scrollTop: number;
  viewportHeight: number;
  bufferSize?: number;
}

interface VisibleRange {
  startIndex: number;
  endIndex: number;
  topPadding: number;
  bottomPadding: number;
}

function calculateVirtualWindow(opts: RenderChunkOptions): VisibleRange {
  const { totalLines, rowHeight, scrollTop, viewportHeight, bufferSize = 5 } = opts;

  const startIndex = Math.max(0, Math.floor(scrollTop / rowHeight) - bufferSize);
  const endIndex = Math.min(totalLines, Math.ceil((scrollTop + viewportHeight) / rowHeight) + bufferSize);

  const topPadding = startIndex * rowHeight;
  const bottomPadding = (totalLines - endIndex) * rowHeight;

  return { startIndex, endIndex, topPadding, bottomPadding };
}

console.log("Virtual Window Range:", calculateVirtualWindow({ totalLines: 20000, rowHeight: 20, scrollTop: 1000, viewportHeight: 600 }));

3. Root cause: CPU computation isolation and DOM node tree inflation

1. Main Thread Blocking: JS execution shares a single thread with UI rendering. Diff algorithms are CPU-intensive; running them synchronously starves the Event Loop. 2. DOM Tree Inflation: rendering 20,000 lines requires creating tens of thousands of <span> elements, consuming hundreds of megabytes of memory. 3. Decoupled Architecture: offload diff computation to Web Workers via postMessage, and use virtual scrolling to restrict physical DOM node counts to 30--50.

4. Recommendation: Web Worker offloading and DOM virtual windowing

1. Web Worker Computation: send raw text strings to a Web Worker thread, receiving structured diff results asynchronously. Use Transferable Objects for zero-copy memory transfers. 2. Virtual Window Rendering: listen to scroll events and dynamically slice visible ranges (slice(startIndex, endIndex)) via requestAnimationFrame.

5. Complete code: Web Worker dispatch and virtual viewport slice helper

This TypeScript snippet demonstrates calculating virtual viewport bounds and slicing visible diff rows.

function computeVisibleDiffChunk<T>(

  allDiffRows: T[],
  scrollTop: number,
  viewportHeight: number,
  rowHeight: number = 22
): { visibleRows: T[]; paddingTop: number; paddingBottom: number } {
  const total = allDiffRows.length;
  if (total === 0) {
    return { visibleRows: [], paddingTop: 0, paddingBottom: 0 };
  }

  const start = Math.max(0, Math.floor(scrollTop / rowHeight) - 3);
  const end = Math.min(total, Math.ceil((scrollTop + viewportHeight) / rowHeight) + 3);

  const visibleRows = allDiffRows.slice(start, end);
  const paddingTop = start * rowHeight;
  const paddingBottom = (total - end) * rowHeight;

  return { visibleRows, paddingTop, paddingBottom };
}

const mockDiffData = Array.from({ length: 10000 }, (_, i) => "Line " + (i + 1));
const windowState = computeVisibleDiffChunk(mockDiffData, 440, 400, 20);
console.log("Physical DOM Nodes Rendered Count:", windowState.visibleRows.length);

6. Incorrect approaches

Attempting setTimeout chunked computation on the main thread (still triggers frequent Long Tasks and extends overall duration). Injecting 10,000 HTML string rows directly via innerHTML. Ignoring scroll event layout thrashing.

7. Boundaries: lazy syntax highlighting on virtualized rows

Syntax highlighting (e.g., Prism / Highlight.js) is also CPU-intensive. Avoid highlighting full files; perform lazy syntax highlighting exclusively on the 30 visible DOM rows rendered in the active viewport.

8. Verification

Monitor Web Worker computation durations using Performance APIs (performance.mark). Inspect Chrome Performance panels to verify that zero main-thread Long Tasks exceed 50ms.

9. FAQ

Will computing diffs in Web Workers freeze the UI? No. Web Workers run in dedicated background threads on separate CPU cores, maintaining 60fps UI scrolling.

How does virtual scrolling support text selection and copying? Mount a hidden textarea with matching height, or intercept copy events (Ctrl+C / Cmd+C) to copy full text from memory to clipboard.

What is the advantage of Transferable Objects? Transferable Objects transfer ArrayBuffer memory ownership with zero-copy overhead, eliminating serialization costs.

10. Summary

High-performance large file diffing requires decoupling computation from UI rendering and binding DOM nodes to viewports. Use Web Workers and DOM virtualization for smooth diff interfaces.

Sources and further reading

Specifications and references for checking the article’s technical details.

Related articles

Keep exploring

Use the related browser tool to test the workflow with your own sample data.

Open related tool