running

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:

WhereEngineRole
WebThe 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, compiledMachine 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, interpretedAn 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:

why compiled scripts are fast

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:

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.

memory growth

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.