Determinism
What at(frame) may depend on, what breaks it, and how host-observed values enter without breaking it.
at(frame) is a pure function of exactly three things:
- the frame number,
- the values of the channel publications it reads,
- the explicit resolution options —
viewport,channelValues.
Nothing else may influence the result. This is a contract the runtime relies on, not a quality it aspires to.
What breaks it
These all compile and all look correct on first playthrough:
// Wall clock — every resolution differs
elapsed = derived(() => Date.now() - this.startedAt());
// Randomness — the same frame renders differently each time
jitter = derived(() => Math.random() * 4);
// Closed-over mutable state — the answer depends on call order
let seen = 0;
index = derived(() => seen++);The failure mode is what makes these dangerous. Forward playback looks fine; the bug shows up as flicker under a distributed render, a wrong frame after a seek, or a cached frame that disagrees with a fresh one.
Same input, same output — including across processes
Remotion renders frames concurrently across processes. A scene that resolves differently in two workers produces a video that is inconsistent frame to frame, with no error anywhere.
Deterministic alternatives
| Instead of | Use |
|---|---|
Date.now() | the frame number, or ClockActor |
Math.random() | a seeded generator whose seed is authored state |
| A measured DOM box | a channel with record: true |
| A stepped simulation | @motionactor/physics record/replay |
| A fetched value | publish once into a channel before rendering |
Why recorded channels exist
A latest-wins publication is wrong for anything that is a function of the frame. Consider a measured box published while rendering forward, then a seek back to frame 10: a latest-wins read returns the measurement from frame 200.
record: true keeps a frame-indexed run log, so a read at frame N returns the value
recorded at the greatest frame ≤ N. Scrubbing backwards replays what was actually
published.
measuredBox = channel<ResolvedLayoutBox>({ record: true });Per-resolution overrides
The cleanest way for a renderer to supply a host value is to pass it per call, so nothing is written at all:
const state = compiled.at(frame, {
channelValues: new Map([[createCellKey(card.id, "availableWidth"), width]]),
});Repeating the frame with the same inputs produces the same output regardless of publication order — which is exactly the property a React render needs, since it may run twice.
What determinism buys
- Seeking without replaying intermediate frames.
- Parallel rendering across processes with no coordination.
- Precise caching —
memoizedSceneinvalidates per channel key. - Headless evaluation — a test or an agent can assert scene state with no renderer at all. See Testing.