Skip to content

Phase 8: wire up Pierre's VirtualizedFileDiff / VirtualizedFile (deferred to #807) #809

@srid

Description

@srid

Why

Phase 8 of #514 calls out large-file virtualization as a known perf gap: a 50k-line lockfile diff currently puts every hunk row into the DOM at once, stalling the frame. Shiki tokenization (post-#708) made the per-row cost cheaper than the old highlight.js path, but row count is still the bottleneck.

@pierre/diffs ships virtualized variants alongside the standard renderers — VirtualizedFile, VirtualizedFileDiff, and the Virtualizer they share — so the fix is to wire them through, not to write virtual scrolling from scratch.

What we found in the spike

Quick read of the type defs (node_modules/.pnpm/@pierre+diffs@*/node_modules/@pierre/diffs/dist/components/VirtualizedFileDiff.d.ts) confirms the swap is not drop-in.

FileDiff constructor:

constructor(
  options?: FileDiffOptions<LAnnotation>,
  workerManager?: WorkerPoolManager,
  isContainerManaged?: boolean,
);

VirtualizedFileDiff constructor:

constructor(
  options: FileDiffOptions<LAnnotation> | undefined,
  virtualizer: Virtualizer,                   // <-- required, shared across files
  metrics?: Partial<VirtualFileMetrics>,
  workerManager?: WorkerPoolManager,
  isContainerManaged?: boolean,
);

Plus extra lifecycle methods that callers have to drive: setVisibility(visible: boolean), reconcileHeights(), onRender(dirty: boolean). The render() signature also differs — VirtualizedFileDiff.render() omits some props that FileDiff.render() accepts (containerWrapper, etc., based on the Omit-style typing).

So the wrapper isn't s/FileDiff/VirtualizedFileDiff/g. It needs:

  1. A Virtualizer instance scoped to the right-panel viewport (one per panel, shared across whatever Pierre instances live inside it).
  2. Visibility plumbing — when the right panel scrolls or resizes, every VirtualizedFileDiff / VirtualizedFile in view needs setVisibility(true), others setVisibility(false).
  3. A reconcileHeights() cadence after content changes (debounced).
  4. The slimmer render() shape on the wrapper's update path.

Where this work belongs

Inside #807 (the packages/solid-pierre extraction), not as a parallel PR. Two reasons:

  • The Virtualizer is a panel-scoped concept — exactly the kind of cross-component lifecycle a wrapper package should encapsulate. Doing it in packages/client/src/ui/Pierre*.tsx first means moving the same code into the new package later.
  • The new package's public API should expose <FileDiff> and <VirtualizedFileDiff> as separate Solid components (or a virtualized?: boolean prop). Designing that surface alongside the rest of the API is cleaner than retrofitting the unvirtualized wrappers and then re-shaping for virtualization.

Scope (deferred until #807)

When #807 picks this up:

  • solid-pierre exposes a host-managed Virtualizer (e.g. a <PierreVirtualizerProvider> that consumers wrap their right-panel tree under, or a hook that returns a Virtualizer ref consumers thread into the components).
  • <FileDiff> and <FileView> get virtualized counterparts — either separate components or a virtualized flag.
  • Visibility is driven by the wrapper's own IntersectionObserver against the panel's scroll container, not punted to consumers.
  • reconcileHeights() debounce sits inside the wrapper; the consumer just sets new content reactively as today.
  • Migration: kolu's CodeTab.tsx flips <PierreDiffView><VirtualizedDiffView> (or sets virtualized={true}) — one site change.

Validation

  • Open a 50k-line diff (pnpm-lock.yaml after a major upgrade is a reliable test case). Frame should not stall.
  • Scroll latency through the diff should stay flat as file size grows.
  • DOM node count for a 50k-line diff should be bounded (~hundreds of rows in flight, not 50k).

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions