Repository navigation
Client-side media processing: animated GIF sub-size generation takes 15-45s per size and produces files larger than the original #80266
Description
Description
With client-side media processing enabled, uploading an animated GIF in the editor can take well over a minute even on fast hardware, with the upload spinner blocking the whole time. Testing with a real-world screen-capture GIF (1517×855, 769 frames, 2.2 MB), the full upload pipeline took 91.5 seconds locally in wp-env.
Instrumenting every worker RPC call shows the time is NOT going where you'd expect:
| Phase | Duration |
|---|---|
hasTransparency (vips; the time is dominated by one-time lazy wasm-vips instantiation in the worker's first call, not the check itself) |
1.2s |
| Upload original GIF (REST) | 1.3s |
thumbnail 150×150 (cropped → static) |
0.04s |
| GIF→video conversion (mediabunny, 769 frames) | 3.6s |
medium 300×169 animated resize (vips) |
~16.5s |
medium_large 766×432 animated resize (vips) |
~27.6s |
large 1024×577 animated resize (vips) |
~44s |
The three uncropped sub-sizes are re-encoded as full 769-frame animated GIFs ([n=-1] in packages/vips resizeImage()), serialized on the single vips worker: ~88s of the 91.5s total. Harness profiling shows ~85% of each resize is the GIF re-encode (per-frame palette quantization in gifsave), not decoding or resizing.
On top of the speed cost, the animated sub-sizes are larger than the original file: the 300×169 medium came out at 5.5 MB from a 2.2 MB source, because downscaling destroys the GIF's inter-frame optimization and every frame is re-quantized.
Cropped sizes (thumbnail) already silently flatten to the first frame (the [n=-1] branch is skipped when cropping), so current behavior is internally inconsistent as well.
What WordPress core does server-side
Core creates sub-sizes for animated GIFs, but always static, from the first frame only:
wp_generate_attachment_metadata()→wp_create_image_subsizes()has no animated special-casing.- GD flattens inherently (
imagecreatefromgif()reads only the first frame). - Imagick explicitly selects frame 1 on load: "Select the first frame to handle animated images properly" (
WP_Image_Editor_Imagick::load()). - At render time,
wp_calculate_image_srcset()keeps flattened sub-sizes and the animated full-size image from ever mixing in asrcset("WordPress flattens animated GIFs into one frame when generating intermediate sizes").
This also means uploads through the wp-admin Media Library (which still take the server path even when client-side processing is active) produce static sub-sizes today, while the editor's client-side path produces animated ones - the same GIF yields structurally different attachments depending on the upload surface.
Proposed resolution
Match core: generate sub-sizes of animated images from the first frame only in packages/vips resizeImage() (drop the [n=-1] uncropped branch). Animation is preserved where it matters - the original full-size GIF is untouched, and the GIF→video flow attaches an animated_video companion.
Measured result with this change (same GIF, same environment): upload drops from 91.5s to 6.9s (13.3×), sub-size generation from ~88s to 0.17s, and sub-sizes shrink from megabytes to kilobytes. Core's existing srcset guards apply to the client-generated static sub-sizes automatically since they key off the GIF mime type at render time.
Alternatives considered and measured, for the record:
- Tuning
gifsave(effort: 2,interframe_maxerror: 8,interpalette_maxerror: 16): 4-8× faster and fixes the size bloat, but still ~35-50s total for three sizes - and still diverges from core. - Skipping sub-sizes for animated GIFs entirely: equally fast, but diverges from core and makes the media library grid fall back to the full-size animated GIF (2.2 MB instead of a 6 KB thumbnail per grid item).
Environment
- Gutenberg trunk (PR Fix upload hang when converting long animated GIFs: decode only the first frame for still outputs #80260 build), wp-env, headless Chromium via Playwright, Apple Silicon
- Reproducible with any long animated GIF; timings scale with frame count × dimensions
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fields✅ Done
Activity