Insight · ·

Two Playing Videos Halved Our Frame Rate. The Fix Was Not the Videos

A moving parent made the second video expensive. One viewport-owned playback slot removed every frame over 33ms without changing the clips.

Two Playing Videos Halved Our Frame Rate. The Fix Was Not the Videos

One playing clip held a 16.7ms median frame. Starting a second clip dropped the same scene to 33.3ms, with 97 percent of frames late. Nothing else on the page had changed.

Those measurements came from our August 2026 work on clodron.com. The test used a pinned horizontal track translating on X while visible, with CPU throttled 6x at 2x device pixel ratio. The obvious suspect was video decoding. It was also wrong.

The trace ruled out JavaScript

Both the one-video and two-video runs contained zero long tasks. The main thread was not blocked by an event handler, an animation loop or React work. Removing code from the scroll callback could not recover time that was not being spent there.

The decisive control test was simpler. We stopped the horizontal track at the same scroll position and left three clips playing. They cost nothing measurable. The same media, same viewport and same decoder load behaved normally while the containing layer stood still. Motion changed the cost.

That result moved the investigation away from the files and toward composition. A performance trace is useful because it can eliminate a plausible story, not only confirm one.

A transformed layer changes the video path

The section pinned a wide track and translated it along the X axis as the page scrolled. Video can usually take a cheap compositor path. Once its containing layer is being transformed continuously, that cheap path may no longer be available. Each additional playing clip then adds expensive compositing work to every moving frame.

The key distinction was not visible versus hidden video. It was playing video inside a layer that was changing. That explains why several stationary clips were cheap and a second moving clip was catastrophic under pressure.

This also explains why reducing JavaScript work did not move the measurement. The late frames were produced after the main-thread work developers usually profile first.

Two sensible fixes changed nothing

We removed a will-change hint. The result did not change. We added contain: paint. The result did not change either. Both were reasonable experiments because layer promotion and paint containment can affect moving sections. Neither addressed the actual video composition path in this scene.

This is the uncomfortable part of browser performance work. A familiar optimization may improve the architecture and still produce no measurable benefit for the failing trace. It stays out of the final explanation unless the before and after numbers move.

We also did not begin by making the encodes smaller. File size matters for transfer and startup. It did not explain why the same decoded clips became free when the track stopped.

Give playback to the viewport

The fix was to create one playback slot. During the moving section, only the card most on screen was allowed to play. The previous clip paused before the next one started. Cards could remain visually complete with poster frames, but the compositor carried one active video through the transform.

That policy took the whole page from 13.8 percent of frames over 33ms to zero. It also made the performance model explicit. Playback was no longer an incidental property of each card. It became a resource owned by the viewport.

The selection needs hysteresis so two nearly equal cards do not rapidly trade the slot. Playback state also has to respond to tab visibility and reduced-motion choices.

Diagnose the moving layer this week

When extra videos suddenly damage scroll, run the stationary control before re-encoding anything. Hold the same scroll position, keep the clips playing and compare frame timing. Check long tasks separately so main-thread and compositor failures do not get mixed.

Then allow only the most visible clip to play while the transformed layer moves. Measure the complete page, not just the isolated section. Keep failed experiments in the performance note. Knowing that will-change and contain: paint changed nothing prevents the next engineer from repeating a plausible but irrelevant fix.

The videos were not individually too heavy. The page had asked the compositor to move several active video surfaces at once. The durable fix was to limit that scarce path, not to keep sanding unrelated code.