https://blog.replit.com/browsers-dont-want-to-be-cameras AgentAgent3 Products Agent Design Database Publish Security Integrations Mobile For Work Pro Replit for serious builders Enterprise Replit with Enterprise-grade security & controls Use Cases Business Apps Mobile Apps Rapid Prototyping Roles Enterprise PM Designers Operations Software Developers Small Businesses SMB owners Founders Resources Get Started Docs Community Expert Network Inspiration Customer Stories Gallery Blog News PricingCareersAgentAgent3 Contact salesLog inSign up * Products * For Work * Resources * Pricing * Careers Contact salesLog in Start building Blog * Engineering We Built a Video Rendering Engine by Lying to the Browser About What Time It Is How Replit turns any web page into a deterministic video by virtualizing time itself, patching key browser audio APIs, and waging war against headless Chrome's quirks. [d01f4f249ff1c45cdc6cb49090cfcf7c629a0cdc-1400x787] Thu, Feb 26, 2026 Updated at: Sat, Feb 28, 2026 Darsh Patel Darsh Patel The Problem: Browsers Don't Want to Be Cameras Here's a deceptively simple product requirement: take a web page with animations, and turn it into a video file. Sounds easy, right? Open a browser. Record the screen. Export MP4. Ship it. We tried that. It doesn't work. The core issue is that browsers are real-time systems. They render frames when they can, skip frames under load, and tie animations to wall-clock time. If your screenshot takes 200ms but your animation expects 16ms frames, you get a stuttery, unwatchable mess. The browser kept rendering at its pace while we captured at ours, and the two never agreed. We needed something more radical. We needed to make the browser believe time moves only when we say it does. Why Not Remotion? Before we go further, a reasonable question: why build this at all? Remotion exists and it's genuinely great. Remotion solves the deterministic rendering problem elegantly: everything is a React component controlled by the library, so it knows exactly what frame you're on and can render any frame in any order. That also unlocks parallel rendering across multiple browser tabs or machines, because frames are independent. We seriously considered it. But our use case has two specific constraints. First, Replit's video renderer takes a URL and produces an MP4. The page behind that URL might use framer-motion, plain CSS animations, raw , or some obscure confetti library. We don't control what's on the page. We just need to capture it perfectly. Remotion gives you determinism by design, but requires you to build inside its component framework. We needed determinism from the outside, applied to arbitrary web content. Second, our videos are generated by an AI agent. Constraining the agent to Remotion's component model would mean teaching it one library's idioms instead of letting it use the entire web platform. The less framework surface area the agent has to reason about, the better the output. So: no special framework. No library buy-in. Just a URL. This meant building the hard thing: making an arbitrary browser environment deterministic after the fact. Freezing Time: The Virtual Clock The heart of our video renderer is a JavaScript file (roughly ~1,200 lines at time of writing) that gets injected into every page we capture. Its job is simple and audacious: replace the main time-related APIs in the browser with a fake clock we control. We replace setTimeout, setInterval, requestAnimationFrame, Date, Date.now(), and performance.now(). In practice, this covers the major JavaScript timing primitives most animation code relies on. The page thinks time is passing normally. In reality, time advances by exactly 1000/fps milliseconds per frame, and only when we tell it to. This means a 60fps animation that takes 500ms per frame to actually render will still produce a butter-smooth 60fps video. The page never knows the difference. From its perspective, each frame takes exactly 16.67ms, always. [2ca1e36c729235b530bfdcc73ef333d9e835655d-2106x2380] The frame loop looks like this: nextFrame() { const loop = async () => { await seekCSSAnimations(currentTime); // sync CSS await seekMedias(); // sync videos currentTime += frameInterval; // tick the clock callIntervalCallbacks(currentTime); // fire setInterval callTimeoutCallbacks(currentTime); // fire setTimeout callRAFCallbacks(currentTime); // fire rAF await captureFrame(); // screenshot loop(); // next frame }; loop(); } Advance clock. Fire callbacks. Capture. Repeat. Every frame is deterministic, every time. The Compositor Warmup Problem (Or: Why We Render Invisible Frames) We discovered a fun bug during development: if there's any delay between loading the page and starting the recording (we fire a hook at start and end times to only record the portion we need), Chrome's compositor gets into a bad state. The root cause? We drive Chrome's rendering loop frame-by-frame rather than letting it render freely. If no frames are issued for a while, internal buffers go stale. The fix is a warmup loop that continuously issues "skip frames" at ~30fps while waiting for the page to signal it's ready to record: startWarmup() { const warmupFrame = async () => { if (startFlag) { stopWarmup(); return; } await skipFrame(); warmupTimerId = setTimeout(warmupFrame, 33); }; warmupFrame(); } We render dozens of frames that nobody will ever see, just to keep Chrome's compositor from going stale. The