How the lottie-mini compressor works
A technical walkthrough of the compression pipeline that turns a 70 MB Lottie file into an ~800 KB equivalent — entirely in your browser, with no server in the loop. This article documents the design choices, what runs where, and why each one matters.
Stage 0: Reading the file
The user drops a .json or .lottie file onto the page. Plain JSON files are read via the File API's readAsText method into a single string, then JSON.parse'd. The .lottie case is more interesting: the file is a standard zip archive, which we unpack in-browser using the fflate library — a 6 KB pure-JavaScript zip implementation with no native dependency. We extract the first animations/*.json entry, which is the animation proper, and keep the original buffer around so we can re-zip later if the user wants a .lottie output.
Stage 1: Identifying compressible assets
With the JSON in hand, we walk the top-level assets array. Each entry that is an embedded image has a p field starting with data:image/. These are the only entries we'll touch — sub-composition assets are passed through unchanged.
For each image asset we extract: the MIME type (from the data URL header), the base64 payload length (a proxy for the JSON byte cost of this asset), and the binary decoded length (the actual image size after base64 decoding). The asset table on the Inspector page sorts by the first number, because that's what actually drives file size in JSON.
Stage 2: Frame stride
Before re-encoding individual frames, we optionally drop frames at a configurable stride. Stride 2 keeps every other frame (60 fps becomes 30 fps); stride 3 keeps every third (60 becomes 20); stride 4 keeps a quarter. This is the highest-leverage knob in the entire pipeline because dropping frames doesn't just cut the encode work — it cuts the file size proportionally with no per-frame quality loss.
The catch is that we have to rewrite the animation timeline so playback timing stays correct. The Lottie format uses ip (in point) and op (out point) values on each layer, plus a per-layer st (start time). Skipping frames means the surviving frames play faster than originally intended unless we adjust these values. Our pipeline scales them by the inverse of the stride.
Stage 3: PNG decoding
For each surviving image asset, we slice the base64 payload off the data URL, decode it to a Uint8Array, and load it into an OffscreenCanvas viacreateImageBitmap. This step happens in a Web Worker so the main thread stays responsive during the (potentially slow) decoding of dozens of large PNGs.
OffscreenCanvas is well-supported in modern browsers (Chrome, Edge, Firefox, Safari 16.4+). For older browsers we fall back to a regular HTMLCanvasElement on the main thread — slower but functional.
Stage 4: WebP encoding
The decoded pixel data is then re-encoded as WebP. We bundle a WebAssembly build of Google's official libwebp encoder. Why WebAssembly rather than the browser's built-in canvas.toBlob("image/webp")? Two reasons:
- Quality parameter control. Browser implementations differ in how they interpret the
qualityargument. Some maps to libwebp's "method" parameter, some to a custom internal heuristic. Bundling libwebp directly gives us deterministic output. - Alpha handling. Lottie embedded PNGs frequently contain alpha channels. The browser's encoder has historical bugs around alpha (especially in lossless mode) that libwebp doesn't.
The encoder runs at user-selectable quality 30–100 (default 75). Quality 75 typically achieves 80% size reduction versus the source PNG with no perceptible quality difference for animation frames. Lower quality compresses more but starts to show ringing artifacts on hard edges below quality 50.
Stage 5: Re-inlining
The output WebP binary is re-encoded as base64 and assembled into a new data URL (data:image/webp;base64,...). The p field of the original asset is replaced with this new URL. If the asset's MIME type changed (PNG → WebP), we also update any nested type hints in the asset entry.
Stage 6: Timeline finalization
With all surviving frames re-encoded, we make one final pass over the layer tree to apply the frame-stride scaling to ip, op, st, and any time-mapped keyframes. The composition's top-level op (total duration) is also adjusted so the animation runs the same wall-clock duration as the original.
Stage 7: Serialization and download
The modified JSON is serialized with JSON.stringify. For .json input, we wrap it in a Blob and offer a download. For .lottie input, we re-zip using fflate, replacing the original animation entry while preserving the manifest and any sibling image files, then offer that zip as a .lottie download.
What stays out of the pipeline
We deliberately do not touch:
- Bodymovin expressions — these are JavaScript-like code blocks bound to properties. Compression would risk silently breaking interactive animations.
- Shape layer geometry — vector data is essentially free. Touching it for marginal byte savings risks visual degradation.
- Layer order or naming — players sometimes key off layer names for runtime targeting (e.g. color theming). We preserve the original tree exactly.
This conservative scope is why lottie-mini's output drops into existing players without configuration changes. The compressed file looks identical to the original to every Lottie player on the market — only smaller.
Why fully client-side?
Server-side compression would be more efficient per-byte (no JavaScript overhead, no WebAssembly indirection, no browser security boundaries). We chose client-side anyway because:
- Confidentiality. Many of the Lottie files people want to compress are unreleased brand assets. Uploading them anywhere introduces risk the operator may not have authority to accept.
- No size limits. Server-side requires upload bandwidth; very large files time out or get rejected at the gateway. In-browser processing has no such ceiling.
- No infrastructure cost. The site is a static export hosted on Vercel's free tier. There's no compute to pay for, no rate limiting to enforce, no quota to police.
Where the gains come from, quantitatively
For a representative 70 MB Lottie file with embedded PNG frames at quality 75 and stride 2:
- PNG → WebP at q75: 70 MB → ~14 MB (80% reduction on the raster bytes)
- Frame stride 2: 14 MB → ~7 MB (50% reduction on frame count)
- Vector + structural data: ~50 KB, unchanged
- Total: 70 MB → ~7 MB at conservative settings
With the Smallest preset (quality 70, stride 2, target width 600px) the same file typically lands under 1 MB — a 70-100× reduction.
Source code
The compression core lives in the open-source repository at github.com/Alex92908/lottie-mini. The web/lib/lottie-compress.ts file contains the main pipeline; the same logic is also packaged as a Python + PyQt6 desktop GUI in the same repository, useful for batch processing.