A faster, deterministic, maintained fork of Zopfli
In October 2025, Google archived zopfli. The repository is now read-only: no more releases, no more fixes, no more maintainer. That's a reasonable end for Google — the algorithm is twelve years old and does exactly what it set out to do — but zopfli is still quietly load-bearing. It's the best general-purpose DEFLATE encoder there is, and it's still in the hot path of PNG optimizers (zopflipng, advpng, parts of oxipng's pipeline) and static-asset gzip precompression on a lot of build systems.
So the canonical implementation just lost its maintainer, and the thing it does is still worth doing. This is a fork that picks it up — and along the way makes it meaningfully faster, leaner, and (newly) deterministic.
What zopfli is, and why it's slow
Zopfli produces standard DEFLATE/zlib/gzip output — anything that reads
a .gz or a zlib stream reads zopfli's output unchanged. The
trick is purely in the encoder: it runs an iterated
optimal-parse search, repeatedly re-costing the LZ77 parse against an
entropy model and converging on a smaller encoding than a normal
deflater would find. It buys a few percent over gzip -9,
and it pays for it in CPU — often 100× slower. That tradeoff is the whole
point: you run zopfli once, at build time, on assets you'll serve
millions of times.
Because you only pay the cost once, "make zopfli faster" has always been the less interesting half of the problem. The more interesting half turned out to be the cost model.
What this fork changes, and how
Four changes, all preserving the encoder's search behavior (same parse decisions, just computed differently or more cheaply):
An integer fixed-point cost model. Upstream computes
symbol costs — the entropy, the -log2 — in
double, through function-pointer cost callbacks. This fork
replaces that with an integer fixed-point model
(IntLog2Fixed, with precision derived from a per-block cost
shift). That single change cascades into the other three and into the
determinism story below.
A longest-match cache fused into the cost dynamic-program. The optimal-parse search re-walks the same positions every iteration. By carrying match distances forward and fusing the longest-match cache directly into the cost DP, the final path walk never re-searches the hash, and iterations after the first become nearly free — the cost stops scaling roughly linearly with iteration count the way upstream's does.
A variable-length match cache that lets iterations ≥2 skip the per-byte hash rebuild entirely.
Trimmed hot data structures. 16-bit hash tables right-sized to the used bucket count, 32-bit LZ77 cumulative histograms, two precomputed per-symbol arrays dropped in favor of cheap on-the-fly recomputation, and scratch buffers reused instead of re-allocated per call.
The numbers
Measured on an Intel Core i9-8950HK (Coffee Lake), release
-O3 -DNDEBUG, at a matched 15 iterations (--i15
for both — upstream's default), min of 2 runs. Text is concatenated
C/C++ source extended by repetition; binary is incompressible random
data.
| Input | Stock zopfli | This fork | Speedup |
|---|---|---|---|
| 256 KB text | 0.90 s | 0.47 s | 1.9× |
| 1 MB text | 2.71 s | 1.40 s | 1.9× |
| 3 MB text | 8.13 s | 4.22 s | 1.9× |
| 10 MB text | 27.16 s | 13.98 s | 1.9× |
| 256 KB binary | 0.36 s | 0.12 s | 2.9× |
| 1 MB binary | 1.58 s | 0.63 s | 2.5× |
| 3 MB binary | 5.69 s | 1.61 s | 3.5× |
| 10 MB binary | 16.46 s | 5.06 s | 3.3× |
Peak resident memory (/usr/bin/time -l, 3 MB input,
--i200):
| Input | Stock zopfli | This fork | Saved |
|---|---|---|---|
| Text (~3 MB) | 23.1 MB | 15.2 MB | −34% |
| Incompressible binary (~3 MB) | 122.5 MB | 75.4 MB | −38% |
The benchmark corpus and method are documented in the repo so you can reproduce these locally.
The determinism angle (the part I'd actually lead with)
Because the cost model is now pure integer arithmetic, the encoder contains no floating point at all. That has two consequences worth more than the speedup:
- Bit-identical output on every CPU, compiler, and FP
mode. Upstream computes costs with
libm'slog(), whose last bits vary with the platform, the compiler, x87-vs-SSE, FMA contraction, and-ffast-math. Those variations can change which parse the search picks, and therefore the output bytes. This fork's output is reproducible across all of that — verified by building at-O0,-O3, and-O3 -ffast-math -ffp-contract=fastand checking the compressed md5 is identical. If you care about reproducible builds, this is the only zopfli that gives you a stable artifact. - No
libm, no FPU. The core links no-lmand emits no floating-point code, so it's smaller and runs without soft-float emulation on FPU-less microcontrollers.
Honest caveats
Output is not byte-identical to upstream zopfli. It's a valid DEFLATE/zlib/gzip stream any decoder reads, and it's reproducible — but it defines a new canonical encoding. If you diff against golden files made by Google's zopfli, they won't match; regenerate them. Compression ratio is within ~0.02% of upstream (sometimes better).
- The default does more work now. The default
iteration count is
0= auto, which scales the pass count with input size instead of a fixed 15. At default settings wall-clock is higher than upstream's fixed--i15(it's chasing a long tail of sub-0.3% gains). Pass an explicit--i15for upstream-like speed; the per-iteration win above is what you keep. - Single-threaded, by design. No threads, no OpenMP. The wins are algorithmic, not from throwing cores at it.
Getting it
It's a drop-in replacement: same ZopfliCompress API
surface, same CLI flags, standard output. Two migration notes — the
include path is now <zopfli/zopfli.h>, and don't
compare bytes against upstream-produced fixtures.
It's actively maintained — issues and PRs welcome.