The 0ms preview: srcdoc + postMessage, no bundler.
Open CodeSandbox and the preview pipeline looks like this: keystroke → websocket → cloud container → bundler → HMR diff → back to your tab. Every hop adds milliseconds, and a cold container adds seconds. It's a good system. It's also a Rube Goldberg machine for the specific job of showing you what your HTML looks like.
NitroIDE does the same job with one iframe and zero network. Not "low latency" — the network leg is literally 0ms, because there is no network leg. Your code goes from keystroke to pixels inside a single browser tab. This post is the full architecture: how the preview is built, why it never flickers, and the tradeoffs I accepted to get there.
One iframe, not a pipeline
The preview is a single <iframe>. The entire document — your HTML, your CSS in a <style> block, your JS in a <script> block, plus any CDN libraries you've injected — is assembled as one string and assigned to the iframe's srcdoc attribute.
If you haven't used srcdoc much: it tells the browser "parse this string as a complete HTML document," in-process, synchronously. No HTTP request happens. Nothing is fetched, uploaded, or proxied. The first render is just:
That's the entire reason the network latency is zero. There is no server to have latency with.
The anti-flicker swap
Setting srcdoc on the live frame works, but it has a visible cost: the browser tears down the old document and paints the new one, and between those two moments there's a white flash. On a preview that re-renders constantly, that flash is maddening.
So NitroIDE never re-renders the live frame directly. Instead it builds a second iframe offscreen — absolutely positioned over the live one, opacity: 0, pointer-events: none — sets srcdoc on the hidden frame, and only swaps it in when the load event fires:
The old frame stays fully visible until the replacement has finished parsing and painting. The user perceives an instant cut, never a blank. It's a double-buffering trick as old as game rendering, applied to an iframe.
Incremental updates: CSS patches in place, HTML/JS rebuild
Full-document rebuilds are for structural changes. For the thing you actually do 500 times a day — tweak a color, nudge a margin — rebuilding the whole document would reset scroll position, focus, and any JS state. So the editor sends surgical updates through postMessage:
{ type: "update-css" }— the new stylesheet is patched into the live document. No rebuild, no state loss.{ type: "eval" }— the console REPL evaluates commands inside the preview context.
HTML and JavaScript edits take the full rebuild path instead. Markup used to patch document.body.innerHTML in place, but that orphaned any DOM references the page's own scripts had cached — a clock's getElementById handles, for example — freezing live widgets on every keystroke. A clean rebuild is the only correct behavior for structural changes.
Why incremental matters: a full rebuild is cheap for the machine but expensive for you — it resets scroll, focus, and runtime state. Patching CSS in place keeps your context alive while you iterate on styles.
The return path: console, errors, and state come back up
A preview that can't talk back is a black box. The iframe reports into the parent over the same postMessage channel: console.log output, runtime errors with line numbers, and the state-watcher's variable snapshots. The parent renders them in the IDE's console panel.
This channel is also where v25's security work lives. As I wrote about in the v25 hardening post, the parent used to interpolate these messages into innerHTML unescaped — a crafted share link could have achieved full parent-origin XSS. The fix was a strict sanitizer that allowlists only the exact HTML vocabulary the console serializer legitimately produces, with everything else escaped or validated. The preview talks freely; the parent trusts nothing. That asymmetry is the whole design.
The sandbox, honestly
The preview iframe runs with sandbox="allow-scripts allow-modals allow-popups allow-forms". Notice what's missing: allow-same-origin. Since v25, the preview runs on an opaque origin — it cannot read the parent's localStorage, where your saved projects live, even if a malicious share link tries.
I'll name the tradeoff plainly: user code in the preview that touches localStorage throws in Safari. That's the correct posture — CodePen and CodeSandbox behave identically — and it's the price of making share links safe by construction rather than by hope. Our docs Troubleshooting section documents this for anyone who hits it.
One-click CDN injection, still zero backend
Mid-session you can inject Tailwind or any CDN library with one click. Implementation-wise it's the boring kind of elegant: the library's <script>/<link> tags are just folded into the srcdoc document string before render. The browser fetches them like any normal page would. No proxy, no bundler plugin, no config file — the thing a bundler would spend 400ms resolving is a string concatenation.
Why this beats the cloud pipeline on latency
| CodeSandbox | StackBlitz | NitroIDE | |
|---|---|---|---|
| Where code runs | Cloud container | WebContainers (WASM) | Your tab |
| Preview transport | Websocket + HMR | In-browser Node | srcdoc string |
| Network leg | Yes | No (but WASM boot cost) | None |
| Cold start | Seconds | Seconds | ~0 |
Prices/features verified 2026-10-13.
The honest footnote: "0ms" describes the network leg, not rendering. Layout and paint still cost what they cost in every browser — physics doesn't negotiate. But every avoidable millisecond (round trips, bundler transforms, container wake-ups) is gone, and the avoidable ones were always the big ones.
What it can't do
Intellectual honesty section, because "0ms" invites skepticism:
"0ms" is the network leg, not the whole pipeline — the preview still has a debounce plus parse/render time, so keystroke-to-pixels is millisecond-level, near-instant rather than literally zero.
- Re-running JS resets top-level state. The
{type:"eval"}path doesn't preserve your variables across edits — that's precisely why the state watcher exists, to make the reset visible instead of mysterious. - No backend. If your app needs a server, this preview isn't for you. It's a frontend renderer; it doesn't pretend otherwise.
- No real bundling. Import maps work, bare npm specifiers don't. Heavy framework builds with 400 dependencies still need a real toolchain. NitroIDE covers the 90% of frontend work that never needed one.
Try it
NitroIDE is free, no signup, and the preview described above is the actual product: nitroide.com. If you want the security half of this story, the v25 hardening post covers the sandbox design, and the full changelog has the details. Questions about the architecture — there's a newsletter signup in the footer if you want these writeups in your inbox.
Feel the 0ms Preview.
Type and watch it render instantly — no bundler, no websocket, no waiting.
Launch the IDEFrequently asked questions
How does the preview update so fast?
One sandboxed srcdoc iframe, incremental postMessage updates with CSS patched in place, and an anti-flicker double-buffer swap — no bundler, no server round-trip.
Is the network latency really zero?
The network leg is zero because there is no network leg — keystroke to pixels happens inside one browser tab; the remaining cost is local parse and render.
What does the sandbox block?
The iframe runs without allow-same-origin on an opaque origin, so it cannot read the parent's localStorage where saved projects live — even from a malicious share link.
What is the tradeoff?
User code in the preview that touches localStorage throws in Safari — the same posture as CodePen and CodeSandbox — the price of making share links safe by construction.