engineeringrendererdeep-dive

Browsers Are Not Cameras - Why We Built a Bare-Metal Video Engine

For the last decade, programmatic video has relied on a hack.

If you wanted to generate video with code, the standard approach was to spin up a headless web browser like Chromium, load a local web server, render HTML and CSS, and take a screenshot of the page 30 or 60 times per second โ€” once for every frame in the output video.

This works for prototypes. It allows developers to use familiar tools like CSS to style frames. But at an infrastructure level, it is inefficient. A web browser is a massive piece of software designed to handle user input, scroll events, network requests, and complex layouts. Using it to simply draw pixels for a video file is like renting a semi-truck to deliver a pizza.

When we designed Workshop, we realized that to achieve true performant video production, we had to abandon the browser. We had to build a video engine that runs on bare metal.

The Browser Tax

The costs of the browser-based approach are invisible in development but crippling in production.

First is the boot time. Starting a headless browser instance takes seconds. If you are rendering one video, that is fine. If you are generating 5,000 personalized videos for a CRM campaign, those seconds add up to hours of wasted compute.

Second is memory. A single Chrome instance can easily consume 2GB of RAM just to render simple text and shapes. This makes horizontal scaling expensive and rules out lightweight infrastructure like serverless functions entirely.

Third is determinism. Browsers are designed to be fluid. They render text differently depending on the operating system, the installed font libraries, and even the subpixel antialiasing settings of the host machine. In video production, however, you need absolute certainty. Frame 100 must look exactly the same today as it did yesterday, on any machine, anywhere in the world.

The Road to Bare Metal

We did not arrive at our current architecture in one step. Like most infrastructure decisions, it was a series of bets โ€” each one removing a layer of abstraction.

Our first renderer was browser-based. We used Remotion, a respected React-based framework that captures frames from a headless Chromium instance. It worked, and it let us validate the product. But it was slow. Boot times were measured in seconds. Memory consumption was enormous.

Our second attempt bypassed the browser but stayed within FFmpeg's own abstraction layer. We generated massive filter_complex commands โ€” FFmpeg's built-in composition language โ€” to layer text, shapes, and images. This was faster, but filter_complex was never designed for the kind of dynamic, per-frame composition we needed. The commands became unwieldy, debugging was painful, and we were still constrained by what FFmpeg's filter graph could express.

The third attempt was the breakthrough. Instead of asking FFmpeg to compose the video, we decided to paint every pixel ourselves and hand FFmpeg only the raw frames. We would do the thinking. FFmpeg would do the encoding.

The Game Loop Architecture

We looked at how video games solve this problem. Games do not use the DOM. They use a render loop.

They clear the screen, calculate the state of the world, draw the geometry, and repeat. This happens in milliseconds. We applied this exact architecture to video generation.

Instead of a browser, Workshop uses a lightweight Node.js process backed by Skia, a high-performance 2D graphics library, and FFmpeg. Raw RGBA pixel data streams directly from our canvas into FFmpeg's stdin. No temporary frame files. No intermediate formats. One process, one pipe, one output.

This pipeline removes the overhead of the DOM entirely.

Separating Thinking from Painting

The most critical architectural decision we made was to decouple the layout engine from the rendering engine.

In a browser, the engine calculates where to place text (layout) at the exact same moment it draws the text (rendering). This tight coupling is computationally expensive and makes caching nearly impossible.

Workshop splits this into two stages.

The Compiler (The Thinking). When you send a .ws document to our API, our compiler runs first. It resolves as much as possible before rendering begins. It computes approximate bounding boxes using proportional character-width estimates, calculates the spring physics for animations, and pre-resolves colors, font selections, and animation curves. It produces a RenderGraph โ€” a structured JSON specification. Every color is a final hex value. Every animation curve is a final array of keyframes. Layout coordinates are estimated at compile time and refined by the renderer using actual font metrics at render time.

The Renderer (The Painting). Our native renderer is lean by design. It receives the RenderGraph and handles the final-mile work: measuring text with actual Skia font metrics, performing precise text wrapping, auto-sizing elements to fit their containers, and interpolating animation curves frame by frame. It then paints the pixels onto a canvas buffer and pipes the raw RGBA data to FFmpeg.

Because the expensive decisions โ€” color resolution, animation physics, font selection, scene timing โ€” are handled upstream by the compiler, the renderer stays focused on pixel-accurate drawing. It is fast, predictable, and stateless.

The Font Problem Nobody Talks About

One of the hardest determinism challenges in video generation has nothing to do with animation or encoding. It is fonts.

Browsers render text differently depending on the operating system, the font rasterizer, the installed system fonts, and the hinting settings. A paragraph that wraps at 42 characters on macOS might wrap at 41 on Ubuntu. This means two machines can produce visually different videos from the same input โ€” a fatal flaw for any system that needs to guarantee consistency.

We solved this by taking full control of font registration. At startup, our renderer walks the filesystem โ€” vendored font directories, system paths, project-specific bundles โ€” and registers every .ttf and .otf file explicitly with Skia's font backend. A family name override map handles edge cases like variable fonts (where the filename bears no reliable relationship to the font family name). Because both the compiler and renderer use the same vendored font files, and the renderer measures text with Skia's own metrics rather than relying on system-level font rasterizers, the output is consistent. The renderer draws text at exactly the size it was told to, with exactly the font it was told to use, and handles final text wrapping using precise glyph measurements.

The result is pixel-identical text rendering across Alpine Linux, macOS x86, macOS ARM, and any other Node.js 20+ environment.

No GPU Required

Here is where our approach diverges from what you might expect.

Most conversations about high-performance rendering lead to the GPU. And yes, when a GPU is available, our engine takes advantage of it. If the host machine has an NVIDIA card, we use NVENC for hardware encoding. On Apple silicon, we use VideoToolbox. On Linux with VAAPI, we use that.

But the real story is that we do not need any of it.

Our default encoder is libx264 โ€” pure software encoding. The entire pipeline runs on a 2-core Linux server with no GPU, no X server, and no display. It runs on Alpine Linux with musl libc. It runs in a Docker container. This is the actual moat: not that we can exploit GPUs, but that we do not require them. We can scale on the cheapest commodity compute available, and optionally accelerate on better hardware when the economics justify it.

Browsers, by contrast, often struggle to even access GPU acceleration in serverless or containerized environments โ€” and they need the GPU just to render the frames, before encoding even begins.

The Result

Moving to a bare-metal architecture transformed the motion graphic generation pipeline. Here are the real numbers, benchmarked against our own browser-based Remotion renderer.

Boot time dropped by roughly 30x. A Remotion render requires launching a Chromium instance, loading a local dev server, and waiting for React to hydrate. Our native renderer starts a single Node.js process and begins painting frames almost immediately.

Memory consumption fell by over 85%. A single Chromium instance can consume over 1GB of RAM to render simple motion graphics. Our native pipeline operates in a fraction of that, making horizontal scaling dramatically cheaper.

Render speed improved from 10โ€“30 seconds per second of 1080p video (with Remotion) to approximately 1โ€“2 seconds per second of 1080p on a 2-core server. That is not a marginal improvement. It is a category change.

Most importantly, we achieved determinism where it matters. Because we control every pixel, every font, and every calculation โ€” and because the RenderGraph pre-resolves colors, timing, and animation physics โ€” we can guarantee that a Workshop video will render identically on any machine, anywhere in the world. The core pipeline is fully deterministic: same input, same output. Optional post-processing effects like film grain introduce controlled randomness for aesthetic purposes, but the foundational render path produces consistent results across environments.

Why This Matters for Generative AI

We believe this is the only viable path for the future of generative media.

AI models like Sora, Veo, and Runway are extraordinary at generating organic, photorealistic motion. But they cannot reliably render a brand's exact hex color, set text in a specific typeface, or guarantee that a logo appears at precisely the right coordinates. These are deterministic problems, and probabilistic models are not built to solve them.

Workshop occupies the complementary space. The compiler and renderer handle everything that must be exact โ€” the typography, the brand colors, the data visualizations, the layout, the physics โ€” while AI models handle everything that benefits from creativity and fluidity.

To generate video at the speed of AI, you cannot rely on the tools of the web. You need an engine built for motion.