Streaming DeepSeek-R1 in the Browser: The Worker-to-UI Pipeline, Step by Step
Running an LLM entirely in the browser sidesteps server costs, latency, and privacy concerns, but the main-thread/Worker coordination is where most prototypes break. This walkthrough surfaces the exact guards, singleton patterns, and streaming callbacks that prevent UI freezes, infinite loops, and memory leaks.
The third installment of a browser-based LLM series dissects the full communication pipeline between a React frontend and a Web Worker running DeepSeek-R1 via Transformers.js and WebGPU. A state machine in App.tsx drives three UI views—welcome, loading, and chat—while a singleton pattern ensures the tokenizer and 4-bit quantized model download only once. Two useEffect guards prevent the infinite generation loop that would otherwise fire on every streaming update.
Inside the Worker, message routing dispatches check, load, generate, interrupt, and reset commands. The generate path applies a chat template to convert OpenAI-format messages into model-specific tokens, then streams output through a dual-callback TextStreamer: a token-level callback tracks speed and detects the transition from thinking to answering, while a text-level callback pushes decoded fragments back to the UI. KV Cache is saved between turns to cut attention computation from O(n²) to O(n) and cleared on reset to free GPU memory.
An InterruptableStoppingCriteria singleton, shared between the message handler and the generation loop, lets a user click stop and immediately halt token production. The complete sequence from pressing Enter to the final decoded output is traced end-to-end.
Browsing the full sequence diagram reveals that the entire streaming UX hinges on a single `useEffect` dependency array and two early returns—remove either guard and the app either never generates or loops infinitely.
The dual-callback TextStreamer design is a pragmatic split: raw token statistics stay cheap and synchronous, while decoded text pushes are batched to avoid flooding the main thread with postMessage calls.
Storing the Worker instance in a useRef rather than useState is a small decision with outsized impact; putting it in state would trigger re-renders on every message, degrading UI performance during streaming.
Using `??=` for model singleton initialization is elegant but brittle—any code path that resets the static properties would trigger a re-download with no recovery path visible in this architecture.