数据压缩(DEFLATE)

由 Rust + WebAssembly 手写 LZ77 + 哈夫曼编码,输出标准 DEFLATE 格式

示例:
或把任意文件拖到这里 / 点击选择

LZ77 匹配过程(前 600 字节)

原样字面量 往回引用(长度, 距离)

结果与浏览器内置的 CompressionStream 互相解压校验  |  鼠标悬停在高亮片段上可以看到它引用的位置

📖 原理说明

ZIP、gzip、PNG 和网页传输里的 HTTP 压缩,背后都是同一个算法:DEFLATE(RFC 1951)。它分两步:先用 LZ77 把重复出现的内容换成「往回数 d 个字节、复制 l 个」的引用,再用哈夫曼编码让常见的符号用更短的比特表示。这个示例完整实现了压缩和解压,输出的是标准格式,能被任何 zlib、浏览器或解压软件读取。

🧮算法原理

平均码长 ≥ H = −Σ pi log2 pi哈夫曼编码逼近香农熵下限
LZ77 与哈希链
从左到右扫描,对当前位置的前 3 个字节做哈希,在哈希表里找到最近几个以相同 3 字节开头的位置,逐一比较,取最长的匹配(最长 258 字节,最远 32 KB)。压缩级别越高,沿哈希链找得越远,越慢但越小。
惰性匹配
4 级以上会多看一步:如果下一个位置能找到更长的匹配,就先把当前字节原样输出,把匹配留给下一个位置。这能让压缩率再提高几个百分点。
哈夫曼编码
字面量(0–255)、结束符和 29 种长度区间共用一张 286 符号的码表,32 KB 内的距离另用一张 30 符号的码表。按出现频率建哈夫曼树,出现越多的符号码越短;码长上限 15 位,超出时把频率减半重建,直到满足限制。
块与三种编码
每 16384 个符号组成一个块,分别估算三种写法的比特数:原样存储(适合随机数据)、标准预定义的固定码表(适合很短的数据)、为这个块专门生成的动态码表(通常最小),选最小的那种。动态码表本身也要压缩:码长序列先做游程编码,再用一张小哈夫曼表编码。

🔄Rust 与 JavaScript 的分工

  1. JSJS 把文字转成 UTF-8 字节(或读取拖入的文件),调用 deflate(数据, 级别)。
  2. RustRust 做 LZ77 匹配、为每块选择最优编码并按位输出,得到原始 DEFLATE 流;deflate_stats() 返回字面量、匹配和各类块的数量。
  3. JSJS 再用浏览器内置的 CompressionStream('deflate-raw') 压缩同一份数据,然后交叉校验:浏览器解压 Rust 的结果,Rust 的 inflate() 解压浏览器的结果,都必须还原成原始数据。
  4. Rustlz77_tokens() 返回前 600 字节的匹配过程,JS 把往回引用的片段高亮显示。

⚡性能要点

  • 测试时与 Python 的 zlib 做了双向对照:1,360 次「本实现压缩 → zlib 解压」和 1,088 次「zlib 压缩 → 本实现解压」全部还原,覆盖空数据、随机字节、长游程、源代码和中文文本。
  • 6 级压缩下,同一批数据的总大小与 zlib 6 级相差不到 0.1%。页面上几十 KB 的示例中,Rust 版耗时通常不到 1 毫秒,浏览器的 CompressionStream 反而要几毫秒:它是异步的流式接口,数据量小时,创建流和调度的开销占了大头。两者的压缩率基本相同。
  • 随机字节几乎无法压缩,此时编码器会选择「原样存储」块,结果只比原数据多每 64 KB 5 个字节,不会越压越大。

源码crates/tools/compress/src/lib.rswww/compress/index.js