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: HTML/CSS patch in place, only JS re-runs
Full-document rebuilds are for structural changes. For the thing you actually do 500 times a day — tweak a color, nudge a margin, fix a typo — 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: "update-html" }— same treatment for markup edits.{ type: "eval" }— JavaScript changes are re-evaluated inside the preview context.
No bundler sits anywhere in this path. Your <script> runs as your script — no transpilation, no module graph, no dev-server transform. That's a deliberate scope decision: NitroIDE is a frontend preview, not a build farm. What you type is what runs, which is exactly why the feedback loop feels instant.
Why incremental matters: a full rebuild is cheap for the machine but expensive for you — it resets scroll, focus, and runtime state. Patching CSS and HTML in place keeps your context alive while you iterate.
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 |
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:
- 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 IDE