Recording video with Puppeteer (and what to use instead)
Puppeteer can record video — sort of. Here's the screencast API, its limits, and the deterministic alternative for real production work.
Every other week, someone files a ticket that says "we need to record a webpage to MP4." Half the time the answer they want to hear is "use Puppeteer" — it's familiar, it's already in their CI, it has the word "browser" in the docs. Sometimes that's the right call. Often it isn't.
What Puppeteer can actually do
The screencast API works. It produces a webm stream:
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto("https://example.com");
const recorder = new PuppeteerScreenRecorder(page);
await recorder.start("./out.mp4");
await page.waitForTimeout(8000);
await recorder.stop();For a quick demo of a static page or a hand-driven flow, this is enough. The output is a video. It plays. Ship it.
Where it breaks
The catch is that screencast captures real-time playback. The CPU has to keep up with the wall clock. The instant your animation is even slightly more expensive than 60fps can handle — a heavy WebGL canvas, a 200-particle confetti burst, a large SVG with many filters — Puppeteer drops frames. The MP4 plays at the right duration but it's missing frames. The animation stutters.
The second catch: timing is non-deterministic. Two runs of the same script, on the same machine, produce slightly different MP4s. CSS animations bind to wall time; setInterval fires whenever the event loop felt like it. PSNR-diff two recordings — they are never identical.
The third catch: anything that depends on fonts, images, or third-party assets has to be loaded and stable before recording starts. Puppeteer's page.waitForLoadState is not enough. You end up writing brittle "wait 800ms for fonts" code.
What "deterministic" buys you
A deterministic renderer flips the relationship between time and capture. Instead of capture chasing the wall clock, the renderer advances time one frame at a time, settles the DOM, and captures.
The result is a MP4 where:
- Every frame is the intended frame at that time index.
- Two renders produce bit-identical output.
- Animation duration is not constrained by CPU speed — a 60-second video can take 90 seconds to render, frame by frame, and the playback still runs at 60fps.
The CSS rewrite
The cost of switching is that your animations have to be expressible as render(t) instead of transition: .... Most CSS animations port directly:
/* Before — wall clock */
.card { animation: pop .8s cubic-bezier(.34, 1.56, .64, 1) forwards; }// After — function of t
function render(t) {
const u = ease(Math.min(1, t / 0.8));
card.style.transform = `scale(${u})`;
}The ease function is the same cubic-bezier, computed analytically. The output is identical. The pipeline is no longer racing the CPU.
When Puppeteer is still right
There is a real use case for Puppeteer recording: capturing flows that include user interaction with real timing — onboarding videos, demo reels of a live app, screen recordings of a real session. For those, the wall-clock nature of the capture is the point.
For everything else — explainers, dashboards, social cards, ads, kinetic typography, OG images, batch personalization — the deterministic path is faster, cheaper, and produces a better video.
A short decision tree
- Recording a live user flow? Puppeteer screencast.
- Recording an animation defined in HTML/CSS/JS? Deterministic renderer.
- "It's complicated"? Probably the second one. If your animation is defined (not interactively driven),
render(t)is the model.
The longer argument is in HTML is the next video codec. The technical comparison is in from DOM to MP4.
The headline
Puppeteer is great for browser automation. It is not great for animation rendering. If your real job is the second thing — and most teams using "puppeteer record video" search queries are — the answer is a renderer designed for it, not a browser automation tool with a screencast accessory.
Cite this postBibTeX · APA · Markdown
@misc{tanaka2026recording,
author = {Kira Tanaka},
title = {Recording video with Puppeteer (and what to use instead)},
year = {2026},
url = {https://hyperframes.video/blog/puppeteer-video-recording},
note = {HyperFrames blog}
}Kira Tanaka. (2026, May 19). Recording video with Puppeteer (and what to use instead). HyperFrames. https://hyperframes.video/blog/puppeteer-video-recording
[Recording video with Puppeteer (and what to use instead)](https://hyperframes.video/blog/puppeteer-video-recording) — Kira Tanaka, 2026
Kira works on the render core: headless Chromium scheduling, frame capture, and the encoder pipeline. She cares about reproducible builds and small numbers next to the word "variance."
Programmatic video generation in Node.js
A walkthrough of generating MP4s from a Node.js process — headless Chromium, frame capture, encoding, and the pitfalls to avoid.
Generate an MP4 from a React component
Render a React component to a deterministic MP4. The trick is treating time as a prop, not a side effect.
How to convert HTML to MP4 programmatically
An end-to-end recipe for turning a static HTML file into a deterministic MP4 — no timeline tool, no manual export, no headless-Chrome plumbing.
Building with HyperFrames? Come hang out.
We're on GitHub, in Discord, and the playground is one click away. Bring weird ideas — we collect them.