Large refactors and migrations sometimes need to arrive as a single change, even when stacked pull requests would otherwise make review easier and reduce delivery risk. The GitHub Copilot app’s rebuilt pull request view targets that difficult case: an enormous diff accompanied by a growing review conversation.
To test the design, the team opened an open source pull request containing 2,200 files, more than a million changed lines, and over 400 inline review comments. Making it responsive required changes to layout measurement, the data pipeline, and the tooling used to reproduce performance failures.
Why comments complicate virtualization
Large code-only diffs have a familiar rendering strategy: virtualize the document, mount only the visible rows and a small surrounding margin, and recycle their DOM elements during scrolling. A document with a million rows can therefore have only about 100 actual rows mounted at a time.
The illusion depends on accurate geometry. Total row heights determine the scrollbar, while cumulative heights determine each row’s position. Variable-height virtualizers can start with estimates and revise them, but code rows with known heights allow the entire layout to be calculated before rendering.
The Copilot app retained its existing foundation: an imperative code-row renderer without a React component for every row, typed arrays for offset calculations, backend-owned diff documents streamed structure-first, and an imperative API for exact navigation to a particular row. Per-frame work does not grow with the total number of rows.
Review threads break that predictable-height model. Markdown wraps according to available width; expandable sections open and close; reply composers grow while users type. Suggested changes, reactions, editing controls, resolution banners, and asynchronously loaded images can all alter a thread’s height.
Fixed reservations based on estimated comment heights are insufficient. Oversized reservations produce blank space, while undersized ones clip content or require nested scrollbars. Updating the shared layout after measuring real content can move everything below it and displace the reader.
Separate geometry for code and conversation
The replacement architecture separates total document height into exact code geometry, effective heights for dynamic blocks, and scroll padding. Code geometry remains deterministic and prefix-summed; a comment resizing does not rebuild it.
Threads, drafts, and reply composers occupy a separate dynamic-block index. Each block has a stable identity that survives content loading and an anchor tied to a file, line, and side rather than a pixel position. Its height fingerprint tracks relevant content and interaction state. Measurement widths are grouped into buckets so ordinary resizing does not invalidate every stored measurement.
A block uses a valid measured height when available, a cached height when its fingerprint and width still match, and an estimate otherwise. This index scales with comments rather than code rows. Several thousand blocks are manageable provided the initial render does not mount or measure them all.
Measure nearby content without creating feedback loops
The initial measurement design let each block’s ResizeObserver write height changes directly into layout. Performance hardening exposed the risk: changing the observed element’s layout can trigger another observation, with costs increasing across mounted blocks.
The shipped approach instead centers on a single measurement pass gated by idle time and scrolling. It waits for the visible range to settle, pauses during active scrolling, and resumes afterward. Only blocks within roughly 2400px of the viewport are candidates, keeping the workload tied to the viewport rather than the document.
For mounted candidates, the pass reads actual rendered heights in a batch, with no intervening writes. Mounted content takes precedence over estimates. That rule eliminated a particularly troublesome blank strip beneath comments: a filtering mistake had excluded mounted blocks from measurement, leaving excessive estimated reservations in place.
For a nearby block that has not mounted, the pass performs at most one off-screen render to refine its reservation before it becomes visible. Blocks taller than the viewport skip this fallback because their excess reservation lies below the visible area and does not justify the extra render.
Mounted blocks still have ResizeObservers to detect changes that may occur independently of scrolling or fingerprint updates, including composer typing, image loading, and expandable-section toggles. Normally, an observer only marks its block for the next idle measurement. Observers disconnect on unmount, and inactive pull request tabs observe nothing.
There is a controlled exception for visible mounted blocks. Waiting until idle after an expansion, composer opening, or image arrival produced a visible two-step change: the comment grew before the code below it moved. The observer can now measure and commit that correction in the same frame before paint. A maximum of one synchronous commit per frame coalesces resize bursts, and active scrolling disables this path in favor of batching.
Preserve the reader’s position by identity
Height corrections also need scroll anchoring. Before changing geometry, the view records the identity of the row or block the reader is anchored to, together with the offset inside it. After applying height changes, it finds that same anchor’s new position and adjusts scrolling to keep it in place.

Changes above the viewport receive compensating adjustments; content loading below it does not. When someone expands a section or opens a reply in a visible block, above-block correction is suppressed for that block so the interaction can naturally push subsequent content downward. Corrections must also avoid fighting active pointer or wheel momentum.
A subtle bug showed why user scrolling must be distinguished from programmatic scrolling. Toggling the file-tree sidebar changes the diff pane’s width. With line wrapping enabled, lines above the reader can reflow, shifting the coordinate system. The surface then emits a small settling scroll of its own.
The original guard treated that programmatic movement as recent user input and blocked the correction needed to preserve position. The current file could consequently drift out of view. Separating user-originated scrolling from the surface’s own side effects fixed the problem.
Keep the data pipeline ahead of the view
Rendering improvements depended on three supporting pipeline choices. First, structural data arrives before full content. Incremental diff requests let the file tree and metadata render while the document is still loading, and the complete set of review threads is resolved up front rather than introduced gradually.
Second, expensive per-item work is delayed until needed. Syntax highlighting runs off the main thread, allowing code to appear immediately as plain text and gain color afterward. Large Markdown bodies and suggested-change context are likewise prepared only as they approach the viewport.
Third, the app balances memory use against return-navigation latency. Releasing large diff documents when navigating away prevents an extended session from retaining every visited document. However, persistent pull request metadata could make the header and file tree reappear immediately while the previously loaded diff remained empty for several seconds.
The team kept bounded retention rather than preserving every document. A cache now holds the last few diffs, evicts older entries, and relies on background refresh to detect stale content. This avoids presenting an instantly restored shell around a diff that must unnecessarily reload.
Find failures with repeatable measurements
Many layout bugs appeared only on large pull requests, at particular scroll positions, or on a particular rendering engine. Some disappeared after scrolling away and returning. Manual observation was therefore a poor basis for diagnosis.
The team added permanent structured probes for the view’s invariants instead of relying on temporary console logs. The probes track mounted row and comment counts, measurement commits per frame, frame duration, scroll-correction size, late comment insertion, and observer cleanup.
Once backend topology has arrived, the expected number of comment blocks inserted after scrolling begins is zero. Other measurements establish whether rendering stays viewport-bound, corrections remain controlled, and observers are properly removed. These signals become pass/fail budgets in an end-to-end test using a synthetic huge pull request with many comments, allowing CI to check the view’s health.
Two unattended testing lanes
A headless probe lane runs declarative interactions against a mock server: opening a pull request, scrolling to a specified fraction, toggling an expandable section, or resizing the window. It reads production instrumentation, including React render counts, the performance timeline, and a requestAnimationFrame sampler for jank.
The probe handles instrumentation, interaction, collection, analysis, and bottleneck ranking automatically. Because its flows are supplied as runtime JSON, an agent can profile a scenario described in plain English without changing source code.
A second autopilot lane operates the actual desktop app through repeated large-pull-request sessions. It tests both cold states, when comments are still skeletons, and warm states with loaded comments. Its interactions include opening expandable sections, opening and cancelling reply composers, collapsing and expanding files, toggling the sidebar, scrolling deep into the file list, and resizing the window.
Measurements are mirrored to an on-disk app log for an agent to inspect without a person operating the interface. Every sample includes a health signal. A warm sample passes only when genuine thread content is mounted, no comment blocks remain blank, and no unfilled gaps appear between comments across the full scroll range, including deep-file sweeps.
The debugging cycle starts with unattended reproduction on the real engine, followed by inspection of health fields rather than visual judgment. When a signal fails, a narrowly targeted structured probe investigates the suspected boundary. Editing the surface hot-reloads the live window and re-arms the autopilot, placing another capture about one cycle away. Once understood, the invariant is recorded in a test and the design document; temporary scaffolding is removed while reliable detection signals remain.
A review surface built for changing conversations
The resulting pull request view is designed to handle a million-line diff with hundreds of threaded comments like a normally sized review. Comments display in full rather than being clipped into internal scrolling boxes. Expanding a section moves the code beneath it, and returning to a recently visited pull request restores the reader’s place.
The central architectural change is treating a review as a conversation whose dimensions evolve during reading, rather than as a fixed-size document. Exact code geometry, separately measured comments, bounded caching, and automated checks work together to keep that changing surface responsive.