Live React Native Preview With No Build Step: Inside RapidNative's 300ms Pipeline
By Riya
6th Oct 2026
Last updated: 6th Oct 2026
The first time you watch an AI model finish a sentence of JSX and see the screen it describes already running on the right half of your editor, your gut reaction is that something has been faked. There is no "Build" button. No spinner. The code hasn't been downloaded, saved, bundled, uploaded, hot-reloaded, or anything else you'd expect from a React Native workflow. It just appears — a tappable, scrollable, actual React Native app, rendered in a browser iframe, roughly 300ms behind the token stream.
A live React Native preview with no visible build step is the single feature every reviewer notices about RapidNative in the first ten seconds, and the one that is hardest to build correctly. The pleasant part — bundling React Native in the browser — is a solved problem; reactnative.run and browser-metro prove it. The unpleasant part is everything around the bundler: how streaming AI writes reach it, what breaks when they arrive mid-render, and why "instant" is less a performance target than a set of invariants you have to defend from four different directions.
This post is a tour of our editor-side pipeline — the one that renders at /project/[id]. We'll walk a prompt from the first streamed token to the pixel on screen, then look at the four invariants that keep that 300ms from quietly ballooning into a reload loop or a blank frame.
A developer watching a mobile preview update live — Photo by Christopher Gower on Unsplash
The Two Previews, and Why This Post Only Covers One
RapidNative ships two independent previews, and conflating them is a reliable way to produce frustrating bugs.
- The editor preview inside
/project/[id]renders on a Lifo sandbox instance. It reads project files from a browser-side virtual file system — the same files the AI agent is writing to. This is the "zero-build" preview we're documenting here. - The device / QR preview runs on orchd workloads serving a bundle to Expo Go on your phone, over a file-sync layer. It is a separate pipeline, with its own quirks (notably: a
package.jsonchange restarts Metro, which drops the Expo Go session until you reload the phone).
Both are important, but only the editor preview can plausibly claim "no build step" from the user's perspective. The device preview still needs the phone to connect and the bundle to ship over the network. Everything below is about the editor pipeline.
What "No Build Step" Actually Means
It does not mean there is no bundler. There is absolutely a bundler. It means there is no bundler the user ever waits on.
Four moving parts collaborate to produce the illusion:
- A Lifo sandbox (
@lifo-sh/core@0.10.18) — a containerized JavaScript runtime running in the browser, with its own virtual file system, a kernel-managed port registry, and aServiceWorkerBridgethat routes HTTP requests via postMessage instead of a service worker. - browser-metro (
browser-metro@1.5.5) — Metro, the React Native bundler, running as a long-lived command inside the sandbox. It watches the VFS and rebuilds on change. - A VFS adapter that lets the editor (and the AI agent's tools) write to the same filesystem Metro is watching — so a tool call like
write_file_contentand a user keystroke in the code editor both end up asvfs.writeFile(...)into the sandbox kernel. - A
PreviewBrowsercomponent (from@lifo-sh/ui) that mounts an iframe pointing at/_sw/<boxId>/<port>/, speaks postMessage with the bridge, and listens forrn-preview-reloadevents to know when to re-navigate.
Put together: the AI writes a file, Metro notices the write, Metro rebuilds, the bridge serves the fresh bundle to the iframe, and React Refresh patches the running tree. No one asked a server for a bundle. No one typed a command. The user watched the right side change while the left side was still streaming.
The browser is the build server, the bundler, and the device — Photo by UX Store on Unsplash
The 300ms Walk-Through
Say the user has just submitted "Build a to-do app" and the model's first tool call writes mobile/app/(app)/index.tsx. Here's what actually happens, in order, roughly:
T=0ms The user hits send. The chat UI dispatches a request to /api/user/ai/generate-v3, which streams Claude via the Vercel AI SDK.
T=50ms The first begin_write_file tool call arrives. The server-side agent primes a pending-write for mobile/app/(app)/index.tsx.
T=100–500ms write_file_content streams token-by-token. Each delta flows down to the client as tool-input-delta. The client enters the fast path: a Redux thunk called saveFile with skipBackend=true, which does not round-trip through the API, and instead writes straight to the browser's VFS via projectVFS.addOrUpdateFile(path, content, mimeType, 'streaming').
T=~150ms First streamed chunk hits the VFS. writeFileToSandbox normalizes the path to /home/user/app/mobile/app/(app)/index.tsx, calls sandbox.kernel.vfs.writeFile(...), and — because this is a new file under /app/ — regenerates __expo_ctx.js, Expo Router's module registry.
T=200–400ms browser-metro's filesystem watcher fires. It transpiles the new TSX, computes which modules changed, and emits something like browser-metro: bundled in 234ms to stdout. On an existing-file edit, the next line would be hot update (3 module(s)). On a brand-new file with no existing HMR accept boundary, there isn't one.
T=~500ms pollReloadUntilRendered(path) dispatches a rn-preview-reload event scoped to this artboard. The SandboxPreview component catches it, confirms the file path matches, grabs the user's current in-app route from __ROUTER_SHIM_HASH__ so it can preserve navigation, and either calls preview.navigate(route) or, if the frame looks blank, preview.reload().
T=~600ms The iframe is now rendering the to-do app. The user is still reading the first paragraph the model generated.
The whole thing is held together by two architectural decisions that are easy to miss. First, the server-side storage adapter is interchangeable with the browser VFS adapter. The same agent tool factories, the same system prompt, the same model-resolution logic — the harness just swaps the IFileStorage implementation. That's what makes the local harness (npm run agent:run) a trustworthy stand-in for the editor. Second, the write path is deliberately asynchronous. Streaming never waits on Metro, Metro never waits on the client, and the iframe navigates on an event rather than polling. If any one of those turned synchronous, the pipeline would stall every time one of the other two had a bad millisecond.
The Four Invariants That Keep "Instant" Instant
The 300ms number is a target, not a property. It survives because four invariants are enforced across the pipeline. Every bug we've seen in this surface came from one of them being broken.
Invariant 1: File writes must not block the token stream
The temptation when you first wire up an "AI writes code" tool is to make write_file_content synchronous — wait for Metro to acknowledge, wait for the preview iframe to render, then unlock the next token. Don't. The AI will outrun any single-threaded consumer, and users will experience a stuttering stream and a stuttering preview at the same time.
The fast path is specifically designed to decouple. The streaming version of saveFile writes to the VFS and dispatches a Redux action, and that's it — no network, no awaiting a bundler, no awaiting the iframe. The second, non-streaming save (the one that persists to our backend) only fires after the full tool call completes. Metro, meanwhile, picks up the VFS write on its own schedule.
If you ever see the preview feel "sticky" to the stream — new lines of code appearing in the chat but the preview falling further behind — the first place to look is whether something in the write path is awaiting a server round-trip that doesn't need to be awaited.
Invariant 2: A brand-new route needs exactly one document reload
This one surprised us. In a traditional dev loop, you edit a file, HMR patches a module, and the app on screen keeps its state. If you add a file — a route that didn't exist when the bundle loaded — HMR can't do anything useful: no running module imports it, so there's no accept boundary to hot-update, and you'll see hot update (0 module(s)) in Metro's stdout while the preview stubbornly refuses to show your new screen.
The way out is to rewrite Expo Router's module context — __expo_ctx.js — to include the new route, let Metro rebuild, and then reload the iframe exactly once. That reload is destructive by design (it discards the running app state), but it has to happen for the new route to be in the module graph.
The two failure modes here are symmetric and both bad:
- Too few reloads. The route exists in the VFS, exists in
__expo_ctx.js, exists in the bundle, but the iframe is still showing a stale module graph that doesn't know about it. User sees expo-router's "Unmatched Route" screen. - Too many reloads. Someone adds a well-meaning watcher that reloads on every file change. The AI generates ten files in a stream, each triggering a reload mid-render, and the user watches a strobing blank screen. We measured this; it's worse than it sounds.
The invariant: one reload per genuinely new route, scoped to the artboard, dispatched as an rn-preview-reload event with the workspace-relative file path. Mid-stream reloads for edits are forbidden; HMR handles those.
Invariant 3: Metro restarts kill the running app — gate them
Metro happily hot-reloads code changes. It cannot hot-reload its own startup environment. When package.json or app.json changes, dependencies may be different, native config may be different, and the bundle preamble (which bakes in environment variables including things like EXPO_PUBLIC_SUPABASE_URL) is stale. Metro has to restart.
In the editor, a Metro restart means the browser-metro command gets a fresh AbortController, exits, and a new instance boots. The iframe loses its connection to the old port and reconnects to the new one — generally fine, but it does cost you the running app state. On the device preview, a Metro restart is a worse experience: Expo Go on the phone doesn't follow the restart automatically. The old bundle keeps running until the user manually reloads (shake → Reload, or re-scan the QR).
The invariant: restart Metro only when the preamble is stale, and never for a plain code change. In practice this means watching for writes to package.json, app.json, and the Tinbase-DB-URL env var, and letting absolutely everything else flow through the bundler watcher instead.
(This is a surface where the two previews visibly diverge. The editor can survive a Metro restart quietly; the phone can't. If a user tells you their editor preview works but their phone shows the pre-generation template, this invariant is where to look.)
Invariant 4: Reconcile the two path roots, both directions
There are two ways to describe the same file in this system, and they are both correct, and neither one is universally right.
- Metro-root relative:
/app/(app)/index.tsx. This is what Metro's module resolution sees, and what gets stamped on every rendered element asdata-bx-path="/app/(app)/index.tsx:10:5"for the click-to-edit feature. - Workspace relative:
mobile/app/(app)/index.tsx. This is what the file lives at in our monorepo-shaped project, what artboards key themselves by, what the AI agent references, and whatrn-preview-reloadevents carry.
Everywhere these two cross — click-to-edit, hover detection, blank-frame detection, event filtering in SandboxPreview — you have to compare them suffix-wise and in both directions. The number of bugs this has caused is embarrassing: a blank-frame heuristic that compared strings exactly and never matched, a hover-outline that drew on the wrong element because it compared left-to-right instead of right-to-left, a code editor that jumped to line 10 of the wrong file for the same reason.
The invariant: never compare a data-bx-path to an artboard key with ===. Normalize to one root or compare by suffix. Treat the two forms as different representations of the same thing, like a UUID and its short form.
When the Invariants Break: The Debug Tooling
The four invariants are deliberately checkable from the browser console. The hardest category of bug in this pipeline — a screen that doesn't render, doesn't stream, reloads too often, or lands on "Unmatched Route" — crosses four layers (VFS write → Metro rebuild → HMR → iframe), and only a timeline shows which one broke.
We built three tools for exactly this, all dev-only and all behind import() so they're never shipped to production:
window.__rnLog auto-records a millisecond-accurate timeline on every project page. It captures Metro's stdout (bundled in, hot update (N)), every rn-preview-reload event and its decision (scheduled, firing, waiting, gave up), and change-only samples every 500ms of iframe URLs, visible text, contributing data-bx-paths, module-context size, artboards, and route-file byte counts. One __rnLog.download() call produces a JSON file that can be read offline and tells you, with surgical precision, which invariant broke and when.
window.__rnDiag() is the one-shot read-only snapshot — route context, VFS route files with sizes, artboards, what each iframe is showing. Useful mid-stream to answer "is the screen even in the module graph, or is the frame on another route?"
/project/blank is a dev-only editor on the real route, with no database, no auth, and no AI, that replays a captured generation verbatim at its measured streaming cadence of ~477 characters per second. If the harness passes and a real project fails, the defect is in the surrounding conditions, not the write-to-render path — a decisive inference that located five separate bugs during the pipeline's most unstable week.
A pipeline this fast is only debuggable if it records itself — Photo by Mika Baumeister on Unsplash
Why Not Just Bundle on the Server?
The honest answer is that we tried, and the economics didn't work.
Expo Snack-style server bundling is a perfectly reasonable architecture, and if you're building a documentation playground where someone loads one snippet, waits two seconds, and taps around, it's probably the right one. For an iterative AI builder it is the wrong one for three reasons.
First, latency compounds. Every tool call becomes a server round-trip, so a ten-file generation is ten extra round-trips plus ten bundle rebuilds on a shared box. At our streaming rate that's noticeable.
Second, cost compounds. The bundler is one of the most expensive processes in the pipeline. Giving every active editor its own browser-side bundler means we don't pay for them when the user is reading, only when they're generating. On a server-bundler model, idle sessions still cost CPU.
Third, state locality matters. The editor has the VFS; the editor has Redux; the editor knows which artboard is on screen. Moving the bundler off-browser means marshalling that state across a network every time the user types a character — or, worse, serving a stale preview that doesn't match the file list the user is looking at.
Keeping the bundler in the browser isn't trendy; it's load-bearing. For context on how RapidNative generates React Native code, the browser bundler is downstream of a specific agent architecture that assumes the preview is near-free to update.
What This Buys the User
The feature list is short, and the experience is specific:
- Streaming visibility. A screen rendered partway through generation is still interactive. Users debug the AI's work in real time instead of waiting for it to finish and starting over.
- Point-and-edit. Because every rendered element carries a
data-bx-path, clicking the preview tells us which source file to edit — the "point at the broken button and tell me what you want" interaction is impossible without live DOM tagging on a live render. - Honest state. The preview is literally running the project's bundle. There's no snapshot, no screenshot, no mock. If the app crashes, it crashes here. If a database query returns nothing, the UI reflects that. The editor is not showing you a design; it's showing you the app.
- Device preview on top. Because the files that produce the editor preview are the same files synced to the orchd workload that serves the QR code, you can scan with Expo Go and get the same app on your phone, with the same migrations and seed data.
If you want to see the full flow — prompt in, running app out — the fastest path is to start a new project and watch the chat and preview panels run in parallel. The 300ms number is easier to feel than to describe.
Takeaways For Anyone Building This Shape
For teams building something similar — a code-generating AI with a live canvas — the shape generalizes:
- Decouple the write path. Agent writes should not wait on the renderer. The renderer should not wait on the writer. Both should write and read a shared store on their own schedule.
- Treat the module graph as a data structure. When the agent adds a file that creates a new route, you are not editing a file; you are mutating the module graph. HMR cannot hide that from you.
- Scope reloads to artboards, not projects. A reload is a destructive op. If one of your users has four screens open and the agent regenerates one, the other three shouldn't blink.
- Record more than you log. A pipeline this fast is debuggable only in retrospect. The second you can't reproduce a bug by hand, you need a timeline.
The "no build step" claim is marketing shorthand for a pile of engineering invariants. When they hold, the user sees magic. When they break, the user sees a blank screen, which — if you've ever seen a React Native blank screen in production — is the opposite of magic.
For more on how this fits into the larger product, RapidNative's approach to AI-generated React Native code walks through the inputs — prompts, PRDs, sketches, screenshots — that feed into the pipeline we've described here. The preview is the first thing you see, but it's the last thing we built.
Ready to build your app?
Turn your idea into a production-ready React Native app in minutes.
Free tools to get you started
Free AI PRD Generator
Generate a professional product requirements document in seconds. Describe your product idea and get a complete, structured PRD instantly.
Try it freeFree AI App Name Generator
Generate unique, brandable app name ideas with AI. Get creative name suggestions with taglines, brand colors, and monogram previews.
Try it freeFree AI App Icon Generator
Generate beautiful, professional app icons with AI. Describe your app and get multiple icon variations in different styles, ready for App Store and Google Play.
Try it freeFrequently asked questions
What is RapidNative?
RapidNative is an AI-powered mobile app builder. Describe the app you want in plain English and RapidNative generates real, production-ready React Native screens you can preview, edit, and publish to the App Store or Google Play.
Can I export the code?
Yes. RapidNative generates clean React Native and Expo code that you can export at any time. No lock-in, no proprietary format. Hand it to your developers or keep building inside RapidNative.
Is RapidNative free to use?
Yes. You can build apps on the free plan with no credit card required. Paid plans unlock unlimited AI generations, code export, and direct publishing to the App Store and Google Play.
Do I need to know how to code?
No. Most users build apps by describing what they want in plain English. Developers can drop into the code whenever they want more control, but coding is optional.
How long does it take to build an app?
Most users have a working first screen in under a minute. A full MVP usually takes a few hours instead of the weeks or months traditional development requires.