Skip to main content
The same composition always produces the same video. That is the guarantee everything else rests on — it is why automated pipelines, CI tests, and AI-driven editing can be trusted.

Why the same frame always comes out the same

Rendering never plays your video. It asks for one frame at a time, and the answer to “what does frame 90 look like?” depends on exactly one thing that changes: the number 90. So the rules are short:
  • No wall clock. No Date.now(), no requestAnimationFrame, no system timers.
  • No unseeded randomness. Math.random() without a seed gives a different frame every run.
  • No fetching mid-render. Every asset loads before the first frame.
  • Fixed output size. fps, width, and height are locked before frame 0.
  • A finite length. Every composition has a known end.

What happens on each frame

1

Frame clock

The engine works out the time with integer math: time = floor(frame) / fps. Real time is never consulted.
2

Seek

The frame adapter gets seekFrame(frame) and moves every animation, DOM change, and canvas draw to that exact moment. All GSAP timelines are paused and seeked, never played.
3

Capture

Chrome’s HeadlessExperimental.beginFrame grabs the pixels in one atomic operation. No half-painted frames.
4

Encode

FFmpeg turns the captured frames into the MP4 and mixes in the audio from your <audio> and <video> elements.
Every frame adapter follows these same rules. If you write your own, it must follow the determinism contract: seekFrame(frame) gives the same result for the same frame, seeks work in any order, nothing resolves after the frame is committed, and the lifecycle stays initseekFrame (many times) → destroy.

Rule out your machine as a variable

Fonts and Chrome versions differ between computers, so a local render can shift by a pixel from one machine to the next. Render in Docker when you need exact reproducibility:
Docker pins the Chromium version, the font set, and the FFmpeg encoder, so the platform stops being an input. The Rendering guide covers every other option.

Will the preview match the render?

Every frame looks the same in both, because both run the same hyperframe.runtime, the producer’s seek behavior is the single source of truth, and the __playerReady and __renderReady gates hold capture until the composition is fully loaded. Speed is a different story. Preview plays in real time in your browser, so it is limited by your hardware. Render is seek-driven and takes one frame at a time, so it never drops a frame no matter how expensive that frame is. A composition that stutters in preview still renders perfectly — see Performance.