Four seconds of work for every second of video
A ContentRoof video is a React composition. The same code runs twice: in the browser, as the live preview you scrub through while you edit, and on our server, where it becomes an MP4. The standard export path opens the composition in headless Chrome, sets the frame, takes a screenshot, hands the picture to an encoder, and repeats, thirty times for every second.
On a 4-core cloud server that took about four seconds of work for every second of video. A one-minute video kept the machine busy for four minutes, and people waited in a queue behind it.
Figure 1Where the CPU went while Chrome exported
Data
| Part | Count | Share |
|---|---|---|
| Chrome painting | 62 | 63% |
| x264 encoder | 16 | 16% |
| GPU and compositor process | 11 | 11% |
| Browser process | 8 | 8% |
| Node (our code) | 2 | 2% |
Setting the React frame took about 5 ms. The screenshot was about 80% of each frame. Even an almost empty frame cost 85 CPU-milliseconds to capture, compress and encode. The cost was not in our components. It was in asking a browser to paint a page and hand back a picture.
Everything we tried first
We tried to make Chrome cheaper before replacing it.
Figure 2What each idea saved before we replaced the browser
Data
| Time saved | |
|---|---|
| Skip resizing the page per screenshot | 10% |
| Faster x264 preset | 13–18% |
| No moving background | 18% |
| No captions | 15% |
| No glows on the drawings | 12% |
| Lower JPEG quality | nothing |
| Compositor layers | slower |
One change was free: a capture setting made Chrome resize the page around every screenshot and repaint everything. Turning it off gave byte-identical frames and 10% more speed. We kept it.
The rest were trades we didn't want. Faster encoder presets made files two to three times bigger. Removing effects one at a time showed that no single effect dominates: the moving background, the glows, the captions and the scene transitions each cost 10–20%. Giving elements their own compositor layers, the usual advice for smooth animation in a browser, made software rendering slower, because every layer is blended over the whole frame.
Drawing the frames ourselves
So we wrote a renderer that runs the unchanged video components without a browser.
Figure 3The renderer, layer by layer
The components don't know the difference. Underneath them, our own renderer lays the page out and paints it with Skia, the same 2D graphics engine Chrome uses, and several worker processes draw different stretches of the video at once. Anything the renderer can't draw yet (3D, stock footage) is detected before any work starts and goes to Chrome as before.
Surprises along the way
Most of the speed came from things that were slow for reasons nobody would guess, and that only showed up when we timed a single frame step by step. Drawing text from several threads was slower than from one. Some gradients and very small blurs cost ten times what their neighbours did. A single encoder, fed by every worker, quietly became the ceiling. None of them was visible in a profile of the whole export, and each had a simple fix once it was found.
Results
Figure 4Exporting the same 55-second video
Data
| Seconds | |
|---|---|
| 4-core server · Chrome | 218.5 s |
| 4-core server · our renderer | 90.5 s |
| M4 laptop · Chrome | 67.5 s |
| M4 · our renderer, CPU | 28.0 s |
| M4 · GPU drawing, x264 | 16.0 s |
| M4 · GPU drawing and encoding | 13.4 s |
| M4 · the same at 720p | 7.6 s |
On the 4-core server the 55-second video exports in 90.5 s instead of 218.5 s, 2.4× faster, and the file is 24% smaller at the same quality setting (Chrome's frames pass through JPEG and carry its noise). At 720p, which free accounts export, a frame takes 33 ms.
On a laptop with a GPU, drawing with Skia on Metal and encoding in hardware brings the same video to 13.4 s, five times faster than Chrome on the same machine. There the slowest step is no longer drawing but copying 8 MB of pixels back from the GPU for each frame, which is why 720p is twice as fast again.
Does it look the same?
Speed only counts if the video is the same video.
Figure 5The same frame, two renderers
A Chrome
B Our renderer
C Difference ×8Figure 6How close the frames are to Chrome's
Data
| From | To | |
|---|---|---|
| Every frame of the measured export (1,660) | 29 dB | 44 dB |
| Separate check across scene types and templates | 30 dB | 45 dB |
| Same check with GPU drawing | 30 dB | 45 dB |
Every frame of the measured export is within 29–44 dB of Chrome's, 33.4 on average.1 A separate check across every scene type and template gave 30–45 dB. A fresh check of 17 frames for this note showed where the remaining difference lives: frames in the middle of a transition, all soft blur and fades, scored 41–44 dB; still frames full of sharp text scored 28–34. The difference is anti-aliasing at the edges of letters and thin lines, which Skia and Chrome compute slightly differently.2
What's next
The production server still exports on its CPU. The rented machines that render our 3D simulations have GPUs that sit idle during ordinary exports, and the GPU path has not been tried on Linux yet. That is the next measurement.


