SnapFire FSR, Full Stack Runtime for the Web is Publicly Released!
Updated on
DISCLAIMER: Expressed views on this blog are my own.
SnapFire FSR represents the next generation of Native Full Stack Runtimes that will power the Web.
- Official Website powered by SnapFire FSR: https://snapfirers.com/
- SnapFire FSR: https://snapfirers.com/fsr
- Docs Page Subsite: https://snapfirers.com/fsr/docs
- FSR Github Repo | FSR crates.io
PageSpeed for WWW | PageSpeed for Docs Subsite
Benchmarks - HIGH PERFormance Rendering vs Node's V8 and QuickJS.
You write React and TypeScript. What serves it is a Rust binary with no JavaScript engine inside it.
Not a Node sidecar. Not V8 behind a socket. Not QuickJS embedded in the process. No JavaScript runtime in the request path at all and you still write your application in TypeScript with a loader beside each page.
Everything below is what falls out of that once it is actually true, ranked by how little anything else can do it.
1. One product, many applications and no iframe in sight
A growing product outgrows one codebase long before it outgrows one domain. Marketing owns the front, docs owns the docs, billing owns billing and every one of them wants their own repository, their own release cadence and their own build that does not go red because someone else's did.
The existing answers are bad. An iframe and you have thrown away navigation, layout, focus and the URL. Client-side module federation and now three teams ship three copies of React to one browser and hydrate them separately and you are debugging a version skew between two bundles at runtime.
FSR mounts a whole application inside another application's shell, on the server:
[sites.docs]
artifact = "sites/docs"
That is a separate FSR application, with its own config, routes, stylesheet, tests and deploy artifact, appearing at a prefix inside the parent. The host grafts the child's route tree into the parent's root layout, so the child inherits the navbar, the theme and the parent's store with no shared code and no shared build. The user gets one document. One React. One hydration pass. One navigation model, where moving between the two applications is a payload swap and the parent's islands never remount.
Teams ship independently. Users cannot tell.
It is a real artifact, not a symlink and a prayer: fsr sites list prints the site, its prefix and a content hash and publishing a new version re-mounts it without restarting the process.
And it is native. Read https://snapfirers.com/fsr/docs and look at what you are actually reading. Not a static site generator's output. Not Markdown in a template. Not a /docs folder in the main site's routes. A separate application, mounted into the main site's shell, in production, on the project's own site. Every page of it is React lowered to IR and rendered by Rust: no JavaScript engine for the chapter, the sidebar rail or the diagrams, which are TSX components emitting inline SVG and which follow the theme toggle because they are rendered rather than embedded.
The theme toggle is the detail worth pausing on. It lives in the parent application's root layout. The docs site knows nothing about it. Click it on a docs page and the page follows, because the graft hands the child the parent's store along with its layout.
I have not seen another documentation site that is a separately deployed application, mounted inside a second application's shell, with React rendered natively in Rust and no JS runtime serving it. If one exists I would like to read about it.
That is also why it exists. A framework whose own website does not use its hardest feature is telling you something about that feature.
2. Your loader is not interpreted. It is data.
Every other framework in this space runs your loader. FSR reads it.
export async function load(ctx: Ctx) {
const product = await ctx.services.catalog.get({ id: ctx.params.id });
return { product };
}
At build time that is parsed, lowered to an intermediate representation and written into a plan. At request time the Rust host executes the plan. There is no engine evaluating your TypeScript, because by then there is no TypeScript left to evaluate.
The same is true of your components, your actions and your event handlers. setOpen(!open) in an onClick becomes IR.
3. The server does the work once, not once per visitor
Because the build can see your whole component tree, it can tell which parts can possibly change in a browser and which cannot.
A call whose inputs are props only, money(line.price), is computed once in Rust and delivered beside the props. A whole subtree with no handler, no state and no island is rendered on the server, recorded as markup and handed to React as inner HTML. React renders nothing inside it and hydrates nothing inside it.
What is left for the browser is what can genuinely change in the browser and the boot report counts the rest for you, per component.
4. Server islands ship zero component code
<Island when="visible" mode="server">
The browser receives no module for that island. None. The island's initial state rides in its props, the server marks the bound elements and the client mounts it with no React root at all. Every interaction is a round trip that returns a patch.
5. Call Rust from TypeScript, synchronously, with no plumbing
#[native]
impl Digest {
pub fn words(&self, bodies: Vec<String>) -> i64 {
bodies.iter().map(|b| b.split_whitespace().count() as i64).sum()
}
}
const words = native.digest.words({ bodies });
No contract file, no transport, no interceptor chain, no serialization boundary to think about, because nothing crosses one. Rust is synchronous and so is the call, so it returns a number rather than a promise.
The TypeScript declaration is generated from the Rust signature, read with syn before anything compiles, so the two cannot drift. Only what the block marks pub crosses.
6. When it cannot lower something, it tells you at build time
A runtime that interprets your code finds the problem when a user hits it. FSR finds it when you build, and names the line.
The site shows what that means: a loader, an action guard and a server island handler, each with the TypeScript on one side and the IR on the other, plus one body it refuses and the reason. Those IR panes are generated by running the lowerer over real source files on every build, so they are what the current compiler produces rather than what someone pasted last year.
7. No npm. No node_modules. Not in development, not in CI, not in production.
fsr add app react@18.3.1
That vendors a browser-native ESM build, commits it and writes the import map entry. The import map is a build-time allowlist, so a bare import with no entry fails the build rather than 404ing in someone's browser six hours later. Nothing is fetched at deploy time.
Your dependency tree is a directory you can read, in your repository, in your diff.
8. Tests and traces you did not have to assemble
fsr test runs body tests that replay a loader against mocked services, page tests that render over a DOM and route tests that go through the host. None need Node. A service with transport = "mock" and a JSON file beside it is how you develop against a backend that does not exist yet.
Traces are two lines in main and no collector to stand up first:
request 19.92ms ok GET /agents
source layout 16.25ms ok
source agents.layout 16.60ms ok
call fleet.listAlerts 15.99ms ok
call fleet.listAgents 15.54ms ok
render shell#document 1.37ms
render routes/layout.tsx#default 1.16ms
render routes/agents/layout.tsx#default 0.82ms miss
Two loaders ran in parallel, you can see which backend each waited on, rendering cost a tenth of what waiting cost and the render memo missed on the last two. With no collector installed a span is a relaxed atomic load and a branch, so the instrumentation stays in release builds.
The numbers
Criterion, Apple M4 Pro, the same pages through lowered FSR IR against React 18 production renderToString inside QuickJS:
| Page | QuickJS cold | QuickJS warm | FSR | Speedup |
|---|---|---|---|---|
| Product page | 19.7 ms | 471 µs | 129 µs | 3.7x |
| Cart, 3 items | 19.6 ms | 505 µs | 128 µs | 3.9x |
| Catalog, 12 cards | 20.0 ms | 1.77 ms | 999 µs | 1.8x |
The warm column is the fair fight and FSR wins it. The cold column decides anything serverless or freshly scaled and it is not a fight: twenty milliseconds against a hundred and twenty nine microseconds, because there is no interpreter to warm up.
What you deploy
A Rust binary, a config directory and a build output. No runtime to install on the box, no version of Node to keep patched, no node_modules to audit, no memory floor for an idle interpreter.
Start here
cargo install snapfire_fsr_cli
fsr new my-app
fsr dev my-app/app
The docs subsite is the short path and the guide underneath it is the long one.
If you have wanted to write TypeScript and ship a Rust binary, it exists now. The Optimization arc starts now!