ZeroJitter

Stop layout thrashing. Stream LLM tokens without jitter.

0 DependenciesCanvas RendererWeb Worker Layout

DOM Rendering

innerHTML per token
Reflows: 0
Drops: 0
Layout: 0ms

ZeroJitter (Canvas)

fillText() • no reflows
Reflows: 0
Drops: 0
Layout: 0ms
0
Reflows
60fps
Maintained
<1ms
Layout Time
$ npm install zero-jitterGitHub →Pair with StreamMD →

Technical Deep-Dive

I Eliminated Layout Jitter From LLM Streaming — Here's How

Every AI chat app has the same bug. You've felt it. That stuttering scrollbar, the content jumping, the dropped frames when tokens stream in. I spent weeks building a library that makes it physically impossible.

🔴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:

TokenDOM mutationStyle recalcLayout reflowPaintComposite

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

Main Thread
LLM tokens
useZeroJitter hook
postMessage()
Web Worker
Intl.Segmenter → measureText()
Line breaking • CJK • BiDi • Emoji
Returns: lines[], height, widths
onmessage()
CanvasRenderer.paint()<canvas>
AccessibilityMirror<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

MetricDOMZeroJitter
Reflows per token10
Layout time0.3–2ms<0.01ms
Frame drops (@ 100 tok/s)12–300
FPS45–5860 (locked)
Scrollbar stabilityJitteryRock solid

💻Usage

$ npm install zero-jitter

// Drop-in streaming component
import { ZeroJitter, useZeroJitter } from 'zero-jitter';

function Chat() {
const ref = useRef<HTMLDivElement>(null);
const { append, clear } = useZeroJitter(ref);

useEffect(() => {
const sse = new EventSource('/api/chat');
sse.onmessage = (e) => append(e.data);
return () => sse.close();
}, [append]);

return <ZeroJitter ref={ref} font="16px Inter" />;
}

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 →