Proxies
A proxy is a low-resolution copy of a clip that the viewport uses in place of the original, so that heavy footage scrubs at a usable rate. Output never sees it.
Why they exist
Seeking is what makes 4K slow, and it is the only cost that scales with resolution. Measured in Chromium on H.264 yuv420p, GOP 60, 30 samples per cell, median milliseconds:
| 4K | 1080p | 540p | |
|---|---|---|---|
| Seek, random | 171.8 | 36.6 | 17.4 |
| Seek, one frame | 148.0 | 40.9 | 16.3 |
| Upload (WebGPU) | 4.3 | 4.4 | 3.8 |
| Upload (WebGL2) | 0.1 | 0.1 | 0.1 |
Seek is 97.6% of the 4K cost. Upload is flat across a 16× payload range — 31.6 down to 2.0 MB per frame — so it is per-call overhead, not bytes, and re-uploading the full-resolution frame is not what makes 4K slow.
At 148 ms, stepping a 4K clip one frame at a time is a 6.8 fps scrub. Resolution
alone predicts a 3.6× improvement; because the remaining cost is
decode-from-keyframe, the proxy also gets a short GOP (-g 12) on top.
The safety property
A proxy is substituted for the interactive viewport only. Never for output.
That is enforced by polarity rather than by vigilance. The useProxies flag is
absent or false by default, so export, the offline renderer, the export preview
and component thumbnails all reach full resolution by doing nothing. Only the
two interactive viewport hosts opt in. There is no opt-out to forget.
Substitution is pixels only, at a single chokepoint. Size, duration, frame rate, pixel aspect, alpha interpretation, loop count, trim, slip, stretch and time remap all keep reading the original asset. There is no second record to keep in step, so a proxy cannot drift out of alignment with its source.
Using one
The proxy row lives on the asset, next to Interpret Footage ▸ Alpha — the same shape of thing, because the record belongs to the file. One proxy therefore speeds up every layer using that file, in every composition.
The row always states which state the file is in, because a proxy silently in use looks like a quality bug:
| State | Meaning |
|---|---|
| Full resolution | No proxy. The row still says what one would be — 1920 × 1080 for a 4K source — so the action is a known quantity before you click it |
| Generating | An ffmpeg job is running. The asset renders at full resolution meanwhile |
| Ready | A decodable stand-in exists |
| In use | Ready, and the global switch is on. Styled loudly rather than left to be inferred from how soft the picture looks |
| Failed | Generation ran and failed, or the file went missing. The error says why, and renders fall back to full resolution |
The global switch — proxies on or off for the whole project — is repeated on the row rather than buried in a menu.
Generating, attaching, detaching
Generate runs ffmpeg as a child process in the main process, for the same reason the export encode does: the transcode must not compete with the UI thread, because the whole promise is that the asset stays usable at full resolution while it runs. The call returns as soon as the job is queued — import is never blocked.
The target is a halved long edge, at or below 1920, rounded to even dimensions. Alpha sources become VP9 WebM; everything else becomes H.264 MP4.
Cancel is real, not cooperative: the child process is killed. Quitting kills
any survivors, which is the other half of why a generating record is never
persisted — there would be no process behind it after a reload.
Attach points the record at a file you supply yourself. It needs no ffmpeg, which makes it the whole proxy story in browser builds, where generation is absent. Detach revokes the object URL for a proxy we generated, and deliberately does not delete a file you attached.
Every failure path lands the asset back at full resolution rather than in an error state. A failed proxy is a missed optimisation; a broken asset is a lost shot. The enumerated cases: no ffmpeg, encode error, a throwing bridge, an unreadable original, empty output, cancellation, an asset deleted mid-encode, and an asset re-imported mid-encode — where the stale job re-reads the record after every await and stands down rather than overwriting the new one.
When it refuses, and why
Refusals are explained rather than attempted, and the check is pure — so the UI can disable the action and say why without starting anything.
| Reason | Message |
|---|---|
| No ffmpeg | Proxies need ffmpeg, which this build cannot reach. You can still attach one |
| Not video | Only video footage can have a proxy |
| Too small | This footage is already small enough to scrub smoothly |
| Unknown size | This file’s dimensions are unknown, so no proxy size can be chosen |
| Already running | A proxy is already being generated for this file |
| Source unreadable | The original file could not be read |
“Too small” means a long edge at or below 1280. Below that the source already seeks in roughly 17 ms — inside a 30 fps frame budget — so halving it would buy a few milliseconds and cost the disk and the wait.