Where Scripts Run
One module, several engines: the browser’s own engine, machine code compiled ahead of time, or an interpreter. A platform decision, never a rewrite.
Every AnimaScript script compiles to WebAssembly, a portable format that no one engine owns. What runs it depends on where the content lands:
| Where | Engine | Role |
|---|---|---|
| Web | The browser’s own WebAssembly engine (V8, JavaScriptCore, …) | Scripts run directly on the browser’s engine. The web editor previews on the same engine the web runtime ships on. |
| Native, compiled | Machine code compiled ahead of time | Where the platform allows it, such as content bundled with an app. It runs about as fast as native code. |
| Native, interpreted | An interpreter | Where the platform cannot generate machine code, such as content an iOS app downloads. It starts at once, with no compile step. |
Measured speed
Measured on real workloads:
- Box2D, ported to AnimaScript, runs its 240 box pyramid within 1.4 times of the native C library running its own SIMD, on V8, Chrome’s engine.
- Our Draco mesh decoder beats Google’s own decoder, C++ compiled to WebAssembly, on every test mesh on V8, from a 23.5 KB gzipped module, under a quarter of the 106 KB Google’s build needs.
- A physical model piano holding a ten note chord takes about 3 percent of one core on the audio thread, compiled.
- In rive-cli’s live window an edit is running as optimized machine code about a second after you save, and in about a tenth of a second when the build is cached.
Nothing sits between your f32 and the hardware: no type
tags, no boxing, and once compiled, no interpreter dispatch.
Execution budgets
Every call into a script, and a module's top level as it starts, runs under
a time budget: the editor's executionTimeoutMs setting, 1000 ms
by default, and 200 ms in the rive CLI and the runtimes. A call
that overruns it traps with “execution exceeded timeout” and
the host carries on. A bake is one build of a project’s scripts into a
module, and how a module keeps to its budget depends on its bake:
- Debug bakes, every preview among them, carry fuel: a counter at the top of each loop and at each call, which reads the clock every 128 steps, so every engine bounds them.
- Optimized bakes, which
rive <dir> --optimizeand every publish make, ask native hosts for their watchdog, a timer that interrupts a call that runs too long. Browsers have no watchdog, so there an optimized module runs unbounded and a runaway loop hangs the page. - Optimized bakes with
wasmFuelmeter themselves as debug bakes do, so every runtime bounds them, browsers included. SetwasmFuel: trueinrive.yaml, or tick Bound scripts in browsers in the editor's file settings. Fuel costs a few percent in loops in Chrome and in compiled native code, 20 to 60% on the native interpreter, and most in deep recursion: a recursivefibruns about 2.4× slower in Chrome and 2.5 to 2.8× slower compiled natively. It is off by default, and a module baked without it is unchanged. dangerouslyFastbakes compile the budget out; trusted content only, since a runaway script then hangs any host. SetdangerouslyFast: trueinrive.yaml; it cannot be combined withwasmFuel.
Build Settings covers both
rive.yaml keys.
Jobs, workers and audio processors, described in
Threads, run in modules of their own, which carry
fuel exactly when the bake's main module does. Jobs and workers have no time
limit: they run until they return or are stopped. Natively the host interrupts
a stopped job at once, and a stopped worker that never awaits again after a
50 ms grace, with fuel or without, so there their fuel only costs speed. A
browser cannot interrupt a call, so there fuel is what ends them: a stopped
job ends at its next check, a stopped worker that never awaits again at its
first check after the grace, and a job or worker step on the page's own
thread, where a build without threads runs them all, once it passes the call
budget. Without fuel, a stopped job ends only once it polls
Lanes.stopping(), and a stopped worker at its next await. An audio
processor's block has a hard limit, 2 blocks of audio by default: natively the
host interrupts a block past it, in a browser's audio worklet a block with fuel
ends at its next check, and one without fuel is dropped only once it
returns.
The editor bounds everything it runs. Its previews are debug bakes, the
static libraries it plays carry fuel, and a module it did not bake, such as one
inside a .riv a script decodes, starts only under a bound the
editor can enforce: its own fuel, or the desktop editor's watchdog. Any other
module stays off, and the log or the browser console says why: an optimized bake
in the web editor, or a dangerouslyFast bake anywhere. So a
runaway loop stops with an error and never freezes the editor, unless
executionTimeoutMs is 0, which turns the budget off. The editor checks only a file's main module, but a bake gives its
jobs, workers and processors fuel whenever it gives the main module fuel, so in
the web editor too a stopped job or worker ends, and without threads one that
holds the page past the budget fails, except while it runs code from a library
whose script mode is fast.
A script’s module starts with about one 64 KB page of memory and grows on demand. Growing can move the memory, and compiled code finds it again after every call that can grow it, so growing in the middle of a frame is safe on every engine.