Quick answer: Faster's animation runtime renders with Rust compiled to WebAssembly, drawing through the browser's GPU pipeline — because animation lives on a 16.7-millisecond budget, and JavaScript's garbage collector doesn't respect budgets. Rust gives us deterministic memory and near-native math; WebAssembly delivers it to every browser; the GPU does the drawing; and the same rendering core runs server-side for video export, so what you preview is what you get.
Animation has a uniquely unforgiving performance contract. A slow page load is annoying; a stuttering animation is visible failure, sixty times per second, on the most polished part of your site. Which is why web animation historically came with an asterisk — fine for a hover state, risky for anything ambitious, and the first thing cut when the performance audit came back.
We wanted animation that scales from a button micro-interaction to an animated product scene without that asterisk — on any customer's website, on whatever device their visitor happens to hold. This post is the engineering story of how the renderer under the visual motion editor works, and why it's built like a game engine rather than a script. It's the third in our engineering series, after workspaces as repositories and email at scale.
The job: one runtime, any website
A design constraint shapes everything downstream: animations in Faster are files played by a runtime, not code embedded in a site. The editor's output is a portable animation document; the runtime is a single script that any page — a Faster site, or anyone else's — can include to play it. No editor dependency, no build step, no framework assumption.
That portability raises the bar brutally. We don't control the pages our runtime lands on — it has to share a browser with whatever else the page is doing, hold frame rate on a mid-range phone, and never be the reason a site feels slow. A runtime you ship to other people's websites doesn't get to have bad days.
The frame budget, and why JavaScript spends it badly
Sixty frames per second gives you 16.7 milliseconds per frame — for everything: evaluating which properties changed, computing transforms, and drawing. Miss the window and the frame drops; drop frames in a pattern and humans perceive it instantly. Average performance is irrelevant here — animation is judged by your worst frame.
JavaScript's problem isn't speed — modern engines are remarkably fast — it's predictability. Allocation-heavy per-frame math produces garbage, and eventually the garbage collector takes its pause. A few milliseconds of GC at the wrong moment is a skipped frame, and per-frame animation math is exactly the allocation pattern that invites it. You can fight this in JavaScript with heroic object pooling and allocation discipline; at some complexity, you're hand-building a memory manager inside a language designed to hide one.
What Rust and WebAssembly actually buy
Rust has no garbage collector — memory is reclaimed at points the compiler proves, not when a runtime decides. For frame-budget work that's the headline feature: the renderer's frame cost is the work it did, with no ambient tax that arrives unscheduled.
Compiled to WebAssembly, the numeric core — transform chains, easing evaluation, vertex preparation — runs at a consistent fraction of native speed in every modern browser, with none of the warm-up variance of JIT-compiled hot paths. Tight loops over packed, typed memory behave on a phone the way they behaved on the workstation where we profiled them.
The same Rust compiles for the browser and for our servers. That's not just code reuse — it's a correctness guarantee with a user-visible payoff we'll get to.
The pipeline, and the art of the boundary
Each frame: the animation graph — the same one you author by describing — evaluates time, inputs like scroll and pointer, and produces the current state of every animated property. The Rust core turns that state into draw commands, and the GPU renders to the page's canvas through WebGL2. Visual effects run as GPU passes chained on the graphics card — the image never round-trips back through the CPU to get blurred or glowed.
The unglamorous part that makes it fast: respecting the JavaScript↔WebAssembly boundary. Crossing it has a cost, so the design rule is to cross rarely and in bulk — state flows across as packed typed arrays, once per frame, not as thousands of chatty property calls. Most of "why is WASM slow for some teams" is boundary chatter; most of making it fast is designing the data layout so there's almost nothing to say across it.
What the performance looks like in practice
- Flat frame times, not just good averages. The whole architecture optimizes for the worst frame — no GC spikes, no JIT warm-up wobble, GPU doing the pixel work. Smoothness is a variance property, and variance is what we removed.
- Cost that scales with the scene, not the page. Canvas rendering sidesteps the way DOM animation costs compound with page complexity — the renderer's work is proportional to what's animating, not to what the rest of the page is doing.
- A one-time startup cost, paid efficiently. The WebAssembly module compiles as it streams in and is cached after; steady-state is where the budget went. For below-the-fold animations, loading defers until they matter.
- Graceful degradation. On weaker devices the right move is simpler motion, not slower motion — which is a design decision surfaced in the mobile preview, backed by a runtime that respects reduced-motion preferences.
The same renderer, without the browser
Because the core is Rust, it also runs server-side — same evaluation, same rendering logic, no browser attached — which is what powers exporting an animation as video. The guarantee that matters to users: the export is rendered by the same code that played the preview. There's no "it looked different when we rendered it" class of bug, because there's no second renderer to disagree with the first. One animation document, one rendering core, two destinations: your webpage in real time, or a video file rendered frame-perfect in the cloud.
What all this means if you never read this far
You describe a motion, tweak it in the visual editor, publish, and it's simply smooth — on your customers' phones, on pages with other things happening, at scene complexities that used to require an asterisk. The engineering's job was to make itself unnoticeable; the taste rules remain yours.
Key takeaways
- Animation is judged by the worst frame: a 16.7ms budget where predictability matters more than average speed.
- JavaScript's garbage collector is the enemy of that budget: Rust's deterministic memory removes the unscheduled tax.
- WebAssembly delivers near-native math to every browser: the GPU does the pixel work, effects included.
- The real craft is the JS↔WASM boundary: cross rarely, in bulk, as packed typed data.
- Renderer cost scales with the animated scene, not the page around it: that's what makes a shippable third-party runtime possible.
- One Rust core renders both the live preview and the video export: what you see is literally what you get.
Frequently asked questions
Does WebAssembly work in all browsers my visitors use?
Every modern browser — desktop and mobile — has shipped WebAssembly support for years; it's as dependable a target as JavaScript itself at this point. The runtime treats it as a baseline, not an enhancement.
Doesn't shipping a WASM module make pages heavier?
The module is a one-time, cached, streaming-compiled cost — and it replaces the per-frame work that actually makes pages feel heavy. The trade is a small fixed download for flat frame times, and for pages with meaningful animation it's the right side of the trade by a wide margin.
Why not just use CSS animations?
We do, where they're right — simple transitions and entrances on page elements don't need a render engine. The Rust path exists for what CSS can't express: rich canvas scenes, input-driven motion, coordinated multi-element choreography, effects, and anything that must also export as video.
Is this why exports match the preview exactly?
Yes — preview and export run the same rendering core, compiled for different targets. Divergence between "what I saw" and "what rendered" requires two renderers; we made sure there's only one.
What about WebGPU?
We render through WebGL2 today because it's universal; the architecture — Rust core, GPU-resident effects, thin boundary — was designed so the graphics backend can evolve as newer GPU interfaces mature in browsers without touching what animations mean or how they're authored.
If you're building frame-budget systems of your own, the transferable lessons are the boring ones: optimize the worst case, make memory boring, and design your boundaries so there's nothing to chat about. And if you just want the animations — describe one; the machine room will hold the frame rate.