Cached at:
05/29/26, 07:20 PM
# On Rendering Diffs
Source: https://pierre.computer/writing/on-rendering-diffs
## PIERRE COMPUTER COMPANY █
Posted on May 29, 2026 by@amadeus (https://x.com/amadeus)
``
██████╗ ██╗███████╗███████╗███████╗
██╔══██╗██║██╔════╝██╔════╝██╔════╝
██████████╗ ██║ ██║██║█████╗ █████╗ ███████╗
╚═════════╝ ██║ ██║██║██╔══╝ ██╔══╝ ╚════██║
██████╔╝██║██║ ██║ ███████║
╚═════╝ ╚═╝╚═╝ ╚═╝ ╚══════╝
``
``
██╗ ██████╗ ██╗███████╗███████╗███████╗
██║ ██╔══██╗██║██╔════╝██╔════╝██╔════╝
██████████╗ ██║ ██║██║█████╗ █████╗ ███████╗
╚═══██╔═══╝ ██║ ██║██║██╔══╝ ██╔══╝ ╚════██║
██║ ██████╔╝██║██║ ██║ ███████║
╚═╝ ╚═════╝ ╚═╝╚═╝ ╚═╝ ╚══════╝
``
You open a pull request expecting to understand what changed\.
For small and medium changes, everything works\. The code is readable, the files are there, you scroll around, add comments, and it’s all pretty seamless\.
Then you open something larger\. Maybe an agent generated the implementation, tests, fixtures, and snapshots\. Maybe the branch just touched more files than expected\. Either way, the review surface starts to degrade\. It might only show you one file at a time, or require each file to be loaded separately before you can read it, or even make basic navigation feel sluggish\.
Some of these are reasonable trade\-offs for genuinely hard problems\. But they still have a cost: reviewers feel the limits of the tool, and product teams have to build workarounds for these limits\.
Diff rendering matters, but for most tools it is not the product\. The product is what happens around the code: review workflows, automation, agent output, CI results, and collaboration\. Code review should support that work, not become something every team has to build from scratch\.
That is why, about 6 months ago, we releasedDiffs (https://diffs.com/)\. Our goal was to make the code and diff rendering part just work, so teams could spend their time on the product around it\.
Originally we launched with just the basic pieces:`File`and`FileDiff`components\. We quickly got feedback about performance issues, so we followed up with a simple virtualizer that avoided rendering code when it was out of view and an API to move syntax highlighting into worker threads\. The simple virtualizer helped, but it was a stopgap\. There was still a lot of O\(n×m\) complexity, high memory usage, and virtualization blanking\. What was missing was a higher\-level component that could manage an entire review surface and handle the hard problems related to scale\.
That missing layer became`CodeView`: a virtualization\-first component for reviewing code and diffs\. And we built it around a deliberately impossible goal:
> You should be able to just render any diff\.
Not literally, of course\. There are physical limits to browsers, compute, and memory\. But practically speaking, I think we’ve come pretty close, and I’d like to share a bit about how we got there\.
If you find long\-form blog posts boring, go check out the`CodeView`playground atDiffsHub\.com (https://diffshub.com/)where you can pretty much view any PR or diff that GitHub will send our way\. Nearly any diff, at any scale, nearly instantly\.
> diffshub\[dot\]com Take any public diff from GitHub and virtualize it nearly instantly, no matter how large, with DiffsHub\. Built to show off our brand new CodeView component\. To try it out, replace \`github\` with \`diffshub\` in your address bar\.pic\.twitter\.com/5X30YwbpHn (https://t.co/5X30YwbpHn) — Pierre \(@pierrecomputer\)May 20, 2026 (https://x.com/pierrecomputer/status/2057174934674124941?ref_src=twsrc%5Etfw)
You can check out the CodeView component and more in the latest version of the diffs package on npm:@pierre/diffs (https://www.npmjs.com/package/@pierre/diffs), orread the docs (https://diffs.com/docs#codeview)\.
## DIFFS LOOK SIMPLE UNTIL THEY ARE NOT
On the surface, rendering diffs in a browser may not seem very hard\. It’s just text, right? Browsers are purpose\-built to take raw HTML and turn that into something you can look at and interact with\. Code is just text, after all\.
But a good review surface needs more than text\. It needs syntax highlighting, line numbers, annotations, comments, theming, split and unified layouts, wrapping modes, and enough customization to fit into someone else’s product\. Each of those features adds cost and complexity\. Syntax highlighting adds processing time and inflates DOM count\. Comments involve additional layout complexity that we can’t fully control, and they still have to work seamlessly with your existing design system\.
With`CodeView`, we take that per\-file complexity and scale it up; work that was cheap for a single diff now has meaningful cost across a large review\. We can roughly break down the problems into three categories:
- **Rendering**— DOM complexity grows quickly, and the browser can become overloaded while scrolling or interacting with the page\.
- **Processing**— Every file or diff operation gets multiplied, so work that was fast in isolation can become expensive when repeated thousands of times\.
- **Memory**— Large files and diffs get transformed into rendering data structures, which can push against browser memory limits and make garbage collection more frequent\.
Our simple virtualizer helped with some rendering problems, and moving highlighting off the main thread helped with parts of the processing problem\. But`CodeView`needed to treat rendering, memory, and processing as connected parts of the same problem\.
## VIRTUALIZATION
Virtualization, or windowing, is a way of tackling the rendering problem\. In its simplest form, the idea is to only render the part of the content near the viewport\. As you scroll, the virtualizer renders the new content coming into view and removes content that has moved off screen\.
Keeping the DOM small has a lot of benefits: lower memory usage, less layout work, less paint work, and fewer elements for the browser to manage\. The trade\-off is that the virtualizer has to estimate or measure how tall everything is, and it must coordinate those changes dynamically\.
One thing that adds to this complexity is that browsers generally manage scroll compositing separately from JavaScript execution\. This can help scrolling feel more responsive to user interactions, but it also means that JavaScript can easily lag behind scroll updates\. This is often most noticeable when using the scrollbar to make large jumps or scrolling extremely quickly — the virtualizer can’t keep up and you’ll scroll into blank regions before the JavaScript has time to render the updated content\.
Click to see blanking in the old virtualizer
### Common Virtualization Techniques
There are a few common ways to virtualize content in a browser, and each comes with its own set of trade\-offs\.
The most common approach is to create a real scrollable region with the full estimated height of the content, then position the visible items where they belong\. This keeps scrolling native: the scrollbar, momentum, input handling, and accessibility all stay with the browser\. The trade\-off is that the rendered window can fall behind the visual scroll position\. Fast scrolls and large scrollbar jumps can expose blank space before JavaScript has a chance to render the next range\. You can reduce that by rendering a larger buffer outside the viewport, but that gives back some of the DOM, layout, and memory savings that virtualization was supposed to buy you\.
Another approach is to keep the visible content in a sticky or fixed container and update what it shows with`requestAnimationFrame`\. In this model, blanking is impossible: the content container cannot scroll out of view because it’s not moving with the scroll position; it just looks like it is\. However, if JavaScript cannot keep up, then scrolling can hitch or stutter because JavaScript is now part of the render update path\. Browser behavior matters here too\. Safari, for example, currently caps`requestAnimationFrame`at 60Hz even on higher refresh\-rate displays, which makes this approach feel worse than native scrolling on those devices\.
A more extreme version is to emulate scrolling entirely: no native scrollable region, just a custom viewport, a fake scrollbar, and content updated via`requestAnimationFrame`as the user*moves*through the document\. This can avoid browser scroll\-size limits because the scroll position is now your own state, not the browser’s\. But the cost is larger: you now own the details of making scrolling feel native, accessible, and correct across different operating systems and browsers\.
### The Inverse Sticky Technique
For`CodeView`, many of those virtualization trade\-offs were not acceptable\. Native browser scrolling mattered\. WebKit\-based environments needed to feel good because Tauri is a common target for developer tools\. And blanking was not an option\.
This left us stuck between different approaches that weren’t quite right\. After some experimentation and frustration, we figured out a hybrid approach that could keep scrolling native, mostly decouple positioning from`requestAnimationFrame`updates, and make blanking effectively impossible\.
We’ve called our new technique the`Inverse Sticky Technique`, but before we talk about how it works, first a quick primer on how`sticky`positioning works\. The typical use case for sticky positioning is ensuring that section headers in a scrollable list stay in view as you scroll through it\. You set`position: sticky; top: 0`on your section headers and then when they should normally be scrolled out of view, they stay fixed to the top of the scroll view as the content below scrolls underneath\.
Section Title 1\(stuck\)
Item 1
Item 2
Item 3
Item 4
Item 5
Section Title 2\(stuck\)
Item 6
Item 7
Item 8
Item 9
Item 10
Section Title 3\(stuck\)
Item 11
Item 12
Item 13
Item 14
Item 15
Item 16
Item 17
Item 18
Item 19
Item 20
For`CodeView`, we invert the usual sticky behavior\. Instead of pinning the top of the rendered content to the top of the viewport as you scroll down, the bottom edge of the rendered region sticks to the bottom of the viewport when you scroll past it\. When you scroll back up, the top edge sticks to the top of the viewport\.
This gives us native scrolling while the viewport is inside the rendered range\. If JavaScript falls behind, the rendered region sticks to one edge instead of scrolling away and exposing blank space\. We can get that behavior with negative`top`and`bottom`sticky offsets, both calculated with the same formula:`\(contentHeight \- viewportHeight\) \* \-1`\.
So to circle back to the goals we set for ourselves: we preserve native scrolling, render updates do not need to be frame\-perfect to keep scrolling feeling smooth, and even large jumps cannot scroll past the rendered content into blank space\.
``
┌────────────────────────────────────────────────────┐
│ ┌────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Full-height content element │ │
│ │ │ │
│ │ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ │ │
│ │ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ │ │
│ │ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ │ │
│ │ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ │ │
│ │ ▓▓▓▓▓ ▓▓▓▓▓ │ │
│ │ ▓▓▓▓▓ Buffer element ▓▓▓▓▓ │ │
│ │ ▓▓▓▓▓ before virtualized content ▓▓▓▓▓ │ │
│ │ ▓▓▓▓▓ ▓▓▓▓▓ │ │
│ │ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ │ │
│ │ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ │ │
│ │ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ │ │
│ │ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ │ │
│ │ │ │
│ │ ┌────────────────────────────────────────────┐ │ │
│ │ │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│ │ │
┌────────────────────────────────────────────────────────╖
│ ▀ ▀ ▀ ▓▓▓ Browser ▓▓▓ ║
├────────────────────────────────────────────────────────╢
│ │ │ │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│ │ │ ║
│ │ │ │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│ │ │ ║
│ │ │ │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│ │ │ ║
│ │ │ │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│ │ │ ║
│ │ │ │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│ │ │ ║
│ │ │ │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│ │ │ ║
│ │ │ │░░░░░░░░░░░░ ░░░░░░░░░░░░│ │ │ ║
│ │ │ │░░░░░░░░░░░░ Rendered content ░░░░░░░░░░░░│ │ │ ║
│ │ │ │░░░░░░░░░░░░ ░░░░░░░░░░░░│ │ │ ║
│ │ │ │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│ │ │ ║
│ │ │ │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│ │ │ ║
│ │ │ │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│ │ │ ║
│ │ │ │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│ │ │ ║
│ │ │ │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│ │ │ ║
│ │ │ │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│ │ │ ║
╘════════════════════════════════════════════════════════╝
│ │ │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│ │ |
│ │ └────────────────────────────────────────────┘ │ |
│ │ │ |
│ │ │ |
│ │ │ |
│ │ │ |
│ │ │ |
│ │ │ |
│ └────────────────────────────────────────────────┘ |
└────────────────────────────────────────────────────┘
``
> While we were shooting for`impossible to blank`, Safari still found a way to break our hearts\. Under sufficiently aggressive scrolling, it can get backed up at the compositing layer and expose blank space\. It usually takes some work to pull off, but it is still technically possible\.
## SCALABLE LAYOUTS
With virtualization in place, the next problem was calculating the layout and size of the scrollable region\. A virtualizer works best when its estimates are close to reality\. Bad estimates mean more corrective work after render: measuring DOM, updating item positions, adjusting scroll height, and sometimes fixing the scroll offset to keep the current content in place\. The more often that happens, the more likely the page is to stutter or make the scrollbar jump around\.
Fortunately, the first pass is pretty cheap\. Files are basically`lineHeight \* totalLines`\. Diffs are only a little more complex because we already have the parsed line counts and hunk metadata\. From there, we just add the hunk separators into the estimate\. Simplified, it looks like this:`\(lineHeight \* diff\.splitLineCount\) \+ \(diff\.hunks\.length \* hunkSeparatorHeight\)`\.
### Rendering Line Ranges
With our rough estimates in place,`CodeView`can determine which files should be rendered\. From there, each rendered file or diff gets the viewport size and position, and uses that to decide which lines should be rendered internally\.
This architecture came from the previous`Virtualizer`, but`CodeView`pushed us to optimize some of the expensive paths\. The old implementation could end up iterating through a file or diff from the beginning to find where the rendered range should start and end\. For most files and diffs, that cost was effectively invisible\. But once we started testing much larger change sets, it became a problem\. A hunk with hundreds of thousands of lines could become pathologically expensive because the lookup still had to start from zero\.
To work around this, we added a cached`position to line`checkpoint system\. That lets us use binary search to find a closer starting point before d