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:
- A
Virtualizer instance scoped to the right-panel viewport (one per panel, shared across whatever Pierre instances live inside it).
- Visibility plumbing — when the right panel scrolls or resizes, every
VirtualizedFileDiff / VirtualizedFile in view needs setVisibility(true), others setVisibility(false).
- A
reconcileHeights() cadence after content changes (debounced).
- 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
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.jspath, but row count is still the bottleneck.@pierre/diffsships virtualized variants alongside the standard renderers —VirtualizedFile,VirtualizedFileDiff, and theVirtualizerthey 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.FileDiffconstructor:VirtualizedFileDiffconstructor:Plus extra lifecycle methods that callers have to drive:
setVisibility(visible: boolean),reconcileHeights(),onRender(dirty: boolean). Therender()signature also differs —VirtualizedFileDiff.render()omits some props thatFileDiff.render()accepts (containerWrapper, etc., based on theOmit-style typing).So the wrapper isn't
s/FileDiff/VirtualizedFileDiff/g. It needs:Virtualizerinstance scoped to the right-panel viewport (one per panel, shared across whatever Pierre instances live inside it).VirtualizedFileDiff/VirtualizedFilein view needssetVisibility(true), otherssetVisibility(false).reconcileHeights()cadence after content changes (debounced).render()shape on the wrapper's update path.Where this work belongs
Inside #807 (the
packages/solid-pierreextraction), not as a parallel PR. Two reasons:Virtualizeris a panel-scoped concept — exactly the kind of cross-component lifecycle a wrapper package should encapsulate. Doing it inpackages/client/src/ui/Pierre*.tsxfirst means moving the same code into the new package later.<FileDiff>and<VirtualizedFileDiff>as separate Solid components (or avirtualized?: booleanprop). 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-pierreexposes a host-managedVirtualizer(e.g. a<PierreVirtualizerProvider>that consumers wrap their right-panel tree under, or a hook that returns aVirtualizerref consumers thread into the components).<FileDiff>and<FileView>get virtualized counterparts — either separate components or avirtualizedflag.IntersectionObserveragainst the panel's scroll container, not punted to consumers.reconcileHeights()debounce sits inside the wrapper; the consumer just sets new content reactively as today.CodeTab.tsxflips<PierreDiffView>→<VirtualizedDiffView>(or setsvirtualized={true}) — one site change.Validation
pnpm-lock.yamlafter a major upgrade is a reliable test case). Frame should not stall.Related
packages/solid-pierreextraction; this is the natural home for the wiring.