🔴The Problem Nobody Talks About
Open ChatGPT. Claude. Gemini. Any LLM-powered chat interface. Now watch the scrollbar while the model streams a response.
See it? That micro-stutter. The scrollbar jumps. The content reflows. If you're on a slower device, you'll see actual frame drops. It's subtle on short responses, but stream 500+ tokens and it becomes infuriating.
Every single token triggers the same cascade:
At 50 tokens/second, that's 50 full layout reflows per second. Each one forces the browser to recalculate every CSS property, recompute geometry of every element in the render tree, determine what pixels need repainting, and composite the final frame.
🟢The Nuclear Option: Bypass the DOM Entirely
I asked a simple question: What if we never trigger a single layout reflow?
The answer was <canvas>. Canvas rendering uses fillText() — a direct pixel operation that happens in the compositor thread. No DOM nodes to measure. No CSS to recalculate. No layout to reflow. Just math → pixels.
But "just use canvas" is like saying "just rewrite everything in Assembly." You lose text selection, accessibility, responsive reflow, line breaking, and international text support. So I built ZeroJitter — a React component that gives you all of those back while keeping the canvas performance.
⚙️Architecture
<canvas><div aria-live>The key insight: Measurement ≠ Rendering. The expensive part of text layout isn't painting pixels — it's measuring text. Every time you add a word, the browser needs to figure out: Does this word fit on the current line? Where does the next line start? How tall is the container now?
ZeroJitter moves ALL of this math to a Web Worker using CanvasRenderingContext2D.measureText(). The worker segments text via Intl.Segmenter, measures each segment, caches measurements, performs line breaking with pure arithmetic (~0.0002ms per text block), and returns line data to the main thread. The main thread then just fillText()s each line. Zero layout involvement. Locked 60fps.
📊The Numbers
| Metric | DOM | ZeroJitter |
|---|---|---|
| Reflows per token | 1 | 0 |
| Layout time | 0.3–2ms | <0.01ms |
| Frame drops (@ 100 tok/s) | 12–30 | 0 |
| FPS | 45–58 | 60 (locked) |
| Scrollbar stability | Jittery | Rock solid |
💻Usage
$ npm install zero-jitter
That's it. Drop-in replacement. Your streaming goes from janky to buttery.
🎯What Makes This Different
Token Coalescing
Multiple tokens in the same frame are batched into one worker message via requestAnimationFrame.
Stale Response Discarding
Monotonic request IDs ensure out-of-order worker responses never cause visual glitches.
Viewport Culling
O(log n) binary search — only visible lines are painted, even for 10,000-line documents.
Full Accessibility
Visually-hidden aria-live mirror with 300ms debounce. Screen readers announce updates naturally.
Zero Dependencies
The text layout engine is vendored. No external runtime deps. Just React as a peer dep.
International Text
CJK per-character breaking, Arabic/Hebrew BiDi, Thai segmentation, emoji width correction.
The Deeper Problem
Layout thrashing isn't a "nice to fix" — it's a trust destroyer. When users interact with an AI chat app, the streaming response is the primary interface. If that interface stutters, users subconsciously associate the jank with the AI itself. "Is it thinking? Did it freeze? Is something wrong?"
Smooth streaming = perceived intelligence.
Every major AI company will need to solve this as models get faster. GPT-4o streams at ~100 tokens/second. The next generation will be 200+. DOM rendering will break completely at those speeds.
$ npm install zero-jitterStar on GitHub →