Chrome takes a full second to encode one canvas image
Pulling 174 frames out of a video took 2.5 seconds. Pulling 9 frames out of the same video took 11. Fewer frames, more time, on the same file, in the same browser, through the same code path.
The encoding was 1.02 seconds per frame in the sparse case and 11 milliseconds per frame in the dense one. That is a factor of 90, and the number 1.02 is the clue.
The constant, in Blink's own source
In canvas_async_blob_creator.cc, Chromium encodes JPEG and PNG from a canvas inside an idle task. If no idle period turns up, it gives up waiting and encodes inline anyway. The timeout is a named constant:
constexpr int kIdleTaskStartTimeoutDelayMs = 1000;One second. Read from the Chromium source on 9 September 2026, and the same value on Mac, Windows and Linux.
So the question is not why the sparse case was slow. It is why the dense case was fast, and the answer is that a busy main thread never goes idle in a way the scheduler counts, in both cases. The difference is what the thread was doing between frames.
Why fewer frames was slower
Taking 174 frames out of a 174 second video means one frame a second, which is dense enough that the decoder runs flat out and every frame arrives with the previous encode already queued. The encodes pile into the same busy stretch and the idle deadline is never the thing being waited on.
Taking 9 frames means seeking. Each seek flushes the decoder, decodes a short run, hands over one frame, and stops. Between frames the thread is busy with the decode and then briefly not busy at all, and that pattern is the worst possible one: never idle enough to get an idle task, always busy enough to keep the timer running. Every one of the nine frames paid the full second.
The fix is a worker, and the reason is in the same file
The same Blink source says a worker's convertToBlob skips the idle path entirely and encodes on the worker's own thread immediately. That thread has nothing else to do, so there is no scheduler to negotiate with.
Moving the encode into a worker took the 9 frame case from 11.0 seconds to 2.0, and the 174 frame case from 174 seconds to 2.5. The worker holds one bitmap and one canvas at a time regardless of how many frames are being extracted, because exactly one frame is in flight: the next is posted only after the last one comes back.
| Frames wanted | Main thread | Worker |
|---|---|---|
| 9, seeking | 11.0 s | 2.0 s |
| 174, scanning | 174 s | 2.5 s |
Measured in Chrome on macOS, 9 September 2026, on one H.264 1108x720 60 fps file of 173.35 seconds.
The mistake worth admitting
Before any of that, the code had to decide whether to seek to each wanted moment or scan the whole video and keep what lands. The cost of one seek was derived by timing an existing function that decodes and re-encodes every frame: 10,396 frames in 11 seconds, so 945 frames a second.
That number is the round trip, not the decode. Pure decoding runs at about 10,200 frames a second, ten times faster. The formula therefore thought scanning was ten times more expensive than it is, and at 9 samples it chose to seek, which took 9.2 seconds, when scanning the entire video would have taken 3.0.
Time the thing you are actually costing. A number that is wrong by a factor of ten still looks like a measurement.
The tool this came out of
Pull stills out of a video, on your own device, with nothing uploaded. 174 frames out of a three minute recording takes about two and a half seconds.
filetity is built by Adarsh Mishra. Every number here was measured on one machine on one file and is reported with the date it was taken, so if your own measurements disagree, they are probably both right and the difference is worth knowing: support@filetity.com.