Historical recipe — not current setup guidance. Measurements and implementation links below belong to their original workload and revision. Deployment labels and defaults may be obsolete. Use the reviewed performance recipes; browse historical evidence.

AVPlumber · Mixer optimization cookbook

Browser CPU is per painted frame

Implementation links are pinned to develop at c20464a5 (2026-10-09); read against develop at 8fa15c92 on 2026-10-13 and still current. Measurements retain their original workload and scope. Watch the current 192-input L4 demo.

Current scope. These are content- and browser-build-specific measurements. Reducing animation damage changes the workload; it is not a free saving for arbitrary moving pages.

Chromium's cost for a browser window follows the frames the window delivers, not what the page shows: about 3.5 ms of CPU for every painted frame, spread over the renderer, the GPU process and the browser's main process, about 0.2 core per window at 60 paints a second. The demo's browser test page slid its marker without pause, so all 29 windows painted at 60 fps. It now rests 4 s and slides 2 s, as a real graphic does, each window from its own point of the cycle; and the browser image moved from Electron 41 to 44.5.1. Electron went from 5.90 to 2.83 cores and host idle from 28 to 50 % on the 68-input 60 fps show.

Status: Implemented; historical test deployment

Later audit: the per-frame figure is two costs. About 1.8 ms follows each painted frame; the rest is paid on every tick of every window, whether it paints or not. See Per tick and per paint below.

Where one browser window's CPU goes, and the test page's rest-and-slide cycle Top: for each frame a window delivers, the renderer takes about 3 percent of a core, the GPU process about 12 percent (VizCompositor and GL through ANGLE) and the browser's main process about 6 percent (paint event, fd pass, release ack): about 3.5 ms of CPU per frame. Chromium paints only on damage: a static key page at 3 paints a second costs about 7 percent of a core, an animated page at 60 paints a second 21 to 25 percent. With 29 windows sliding without pause: 1,748 paints a second and Electron at 5.90 cores, more than avplumber's 5.08. Bottom: eight of the 29 windows over one 6 s cycle; each rests 4 s and slides 2 s, starting at a golden-ratio step of its id's number, so at any moment about a third are sliding: 3 of the 8 shown, 9 to 11 of 29 measured. Result: 594 paints a second, Electron 3.25 cores, host idle 28 to 49 percent. One window, one delivered frame · ~3.5 ms of CPU across three Chromium processes Renderer · ~3 % paints on damage GPU process · ~12 % VizCompositor, GL through ANGLE Browser main · ~6 % paint event, fd pass, release ack mixer Chromium paints only on damage static key page · 3 paints/s ~7 % of a core animated page · 60 paints/s 21–25 % 29 windows sliding without pause: 1,748 paints/s, Electron 5.90 cores, more than avplumber's 5.08. Test page · each window rests 4 s and slides 2 s, from its own point of the cycle 0 s2 s4 s6 s now browser_001browser_002 browser_003browser_004 browser_005browser_006 browser_007browser_008 grey: resting, no paints · blue: sliding, 60 paints/s · at “now” 3 of these 8 slide; 9–11 of 29 measured 594 paints/s instead of 1,748: Electron 3.25 cores, host idle 28 → 49 %, GPU ~83 → 62 %, 0 missed deadlines.
The per-process shares and the two paint-rate costs are measured per window on the 68-input show. The timeline is an illustration: eight windows out of 29, drawn with the real offsets of ids 1–8 (a golden-ratio step of the id's number), so that the share sliding at “now” is the rule's, not a measurement.

Where a window's CPU goes

For every frame a window delivers, three Chromium processes do work: the renderer paints the damaged page (about 3 % of a core per window at 60 paints/s), the GPU process composites it in VizCompositor and draws through ANGLE's GL (about 12 %), and the browser's main process handles the paint event, passes the DMA-BUF descriptor over the socket and takes the mixer's release ack (about 6 %; see Browser frame hand-off). Together about 3.5 ms of CPU per delivered frame, which at 60 paints/s is about 0.2 core per window (3 + 12 + 6 % = 21 %, computed). The cost tracks delivered frames, not page complexity. Part of the GPU process's share is a copy of every frame into the capture buffer and a poll for its end; a patched Chromium on the L4 paints straight into the buffer the mixer reads instead (Chromium paints into the buffer the mixer reads).

Chromium paints only on damage, so a page that does not change delivers no frame and pays none of the per-frame work. It is not free: its window is still woken on every tick (Per tick and per paint). On the same show a static key page painting 3 times a second took about 7 % of a core, and the always-sliding test page at 60 paints/s 21–25 %. The number of Electron workers does not enter: the work is per window per frame, so grouping windows into fewer, bigger workers would not reduce it and would concentrate VizCompositor load in fewer GPU processes (rejected idea 9). That leaves two levers, and this page pulls both: fewer delivered frames when nothing on the page moves, and a cheaper Chromium.

This is not the shortcut the cookbook rules out. Every window is still an independent 60 fps input: while its marker moves it paints 60 frames a second and the mixer imports, stamps and composites every one; the sender, the chain and the ring are unchanged. What changed is the content: a graphic that holds still two thirds of the time, which is what real lower thirds, bugs and tickers do. Running browser sources at 30 fps in a 60 fps show, which halves the delivered rate of a page that is moving, stays rejected (idea 10).

Measured at 68 inputs, 60 fps

Same 68-input 60 fps show (18 NVDEC, 29 browser sources, 17 raw NV12 files and 4 browser key pages) on the T4 test host (16 vCPU), same avplumber build. Three states, measured as two sequential pairs: the columns are not additive.

MetricAlways sliding, Electron 41Rests 2/3, Electron 41Rests 2/3, Electron 44
Electron CPU5.90 cores3.25 cores2.83 cores
Host CPU idle28 %49 %50.3 %
avplumber CPU5.08 cores4.61 cores4.58 cores
Browser paints/s1,748594–
GPU utilisation~83 %62 %–

The first pair, columns 1 and 2, is the test-page change; the rested page ran with 0 missed program deadlines. The second pair is Electron 41 against 44 on the rested page, measured as its own two runs, so its Electron 41 baseline is not column 2: it read 3.30 cores, 47.7 % idle, avplumber 4.62 cores and CPU pressure (PSI some) 17.6 %, against 2.83 cores, 50.3 %, 4.58 cores and 16.4 % on Electron 44: Electron CPU −14 %, no regression. The gap between that baseline and column 2, 0.05 core and 1.3 points of idle, is within run-to-run noise. PSI was sampled only in the second pair (how it is read).

What changed

  1. The test page rests two thirds of the time. Before, the marker of browser_alpha.html ran a CSS keyframe animation, 4 s each way, without pause, so every window painted every frame. Now the marker's transform has a 2 s transition, and a timer toggles its target every 6 s: slide 2 s, rest 4 s. It moves on transform, not left, which snaps to whole pixels and lays out on the main thread each frame. Each window starts its cycle at (index × 0.618 mod 1) × 6 s, where index is the number in the source's id (browser_007 is 7), which prepare_demo embeds in the page as a data attribute; golden-ratio steps spread any count of windows evenly over the cycle, so 9–11 of the 29 slide at any moment instead of all of them. Paints fell from 1,748 to 594 a second, Electron from 5.90 to 3.25 cores, avplumber from 5.08 to 4.61, GPU utilisation from about 83 to 62 %, host idle rose from 28 to 49 %, with 0 missed program deadlines.
  2. Electron 44.5.1 (Chromium 152) on Fedora 44. Electron 41 (Chromium 146) and Fedora 42 were both end of life. On the rested page, Electron CPU fell from 3.30 to 2.83 cores (−14 %), host idle rose from 47.7 to 50.3 %, avplumber went from 4.62 to 4.58 cores and CPU pressure (PSI some) from 17.6 to 16.4 %, with no regression. Electron 42 and later no longer download the Electron binary in the package's npm postinstall, so the image build runs node node_modules/electron/install.js after npm ci. The image needs only Electron's runtime libraries; the NVIDIA userspace comes from the host through the container runtime. The GPU-process crash on recreating all windows is the same on Electron 41 and 44; its fix is on Keeping Vulkan out of the GPU process.

Per tick and per paint: a later audit on the L4

A 20-minute capture of every browser thread's run time and wake-ups, on the 88-source 60 fps show on the L4 host: 40 windows in 5 workers on Electron 44, 36 test pages and 4 key pages, 864 paints a second in all, 2.86 cores in the browser container. Dividing one by the other gives 3.3 ms per painted frame, close to the figure this page starts with. The capture splits it:

Part of the 2.86 coresPer eventCores
Per painted frame, 864 a second1.5–1.9 ms1.30–1.65
Per tick of each window, 2,400 a second, painting or not0.45–0.59 ms1.07–1.41
Page-specific: the ticker's script loop and five pages that grow–0.13

The split is computed, not measured directly: it is a fit across the five workers, whose paint rates differ only by their key page, so it rests on five points and a long extrapolation to zero paints. One measured fact shows the tick cost without the fit: a key page that paints 2 times a second and a test page that paints 20.3 times a second cost the same on the renderer's Compositor thread, 12.05 against 12.18 thousandths of a core.

Two things are open. Why a resting window ticks at all is not proven: 0.7–0.9 core is spent on ticks that paint nothing, and one test decides whether the page or the browser asks for them, an empty window beside a test page. And five pages' Compositor threads cost more the longer they run, 0.08 core in all after 4.5 hours of uptime; no data here explains it.

At a glance

Where in the code

Source links are pinned to the reviewed revision.

Measured on the T4 test host (16 vCPU) with the 68-input 60 fps show: 18 NVDEC, 29 browser sources, 17 raw NV12 files and 4 browser key pages. The per-window shares are Chromium CPU per window on that show; Electron and avplumber figures are whole-process totals.