uBlock Origin 的 WebAssembly LZ4 压缩库:lz4-block-codec 双实现架构、构建流程与存储压缩实战
本文围绕 uBlock Origin 仓库中的 src/lib/lz4 目录,解析这个实现 LZ4 块格式压缩/解压的 WebAssembly 库:统一入口如何按需加载 wasm 与纯 JavaScript 双实现、wasm 模块的内存布局与增长策略、纯 JS 回退版的哈希表编码细节,以及它如何被 src/js/lz4.js 用于过滤规则资产的存储压缩。读完本文,你能完整理解该库的文件组织、createInstance/encodeBlock/decodeBlock 接口契约、.wat 到 .wasm 的构建命令,以及实例生命周期管理(TTL 释放)的工程考量。
一、库的定位:独立的 LZ4 块格式编解码器
src/lib/lz4/README.md 开篇明确了该目录的用途:实现 LZ4 压缩/解压,其块格式遵循官方 LZ4 仓库中的 lz4_Block_format 文档规范(LZ4 block format,即裸块编码,不含 frame 头)。
README 同时说明,这些文件是作为一个独立项目开发的(gorhill/lz4-wasm),再被 uBlock Origin 以 BSD-2-Clause 许可引入。也就是说,阅读 src/lib/lz4 时应当把它当作一个自包含的微型库:它不依赖 uBlock 的其余代码,只暴露全局命名空间对象,供外部包装调用。
该目录下共五个文件,与 README 的 "Files" 章节一一对应:
| 文件 | 角色 |
|---|---|
src/lib/lz4/lz4-block-codec-any.js |
统一入口:实例化 wasm 或纯 JS 编解码器 |
src/lib/lz4/lz4-block-codec-wasm.js |
WebAssembly 实现包装器 |
src/lib/lz4/lz4-block-codec-js.js |
纯 JavaScript 实现(回退方案) |
src/lib/lz4/lz4-block-codec.wasm |
编译后的 WebAssembly 二进制模块 |
src/lib/lz4/lz4-block-codec.wat |
生成上述模块的 WebAssembly 文本源码 |
二、统一入口:按需加载与 wasm 优先、JS 兜底
README 对 lz4-block-codec-any.js 的描述是:"若未指定实现,会先尝试创建 WebAssembly 实例;若因任何原因失败,则创建纯 JavaScript 实例。两种实现的脚本都是动态加载的,只在需要时加载,避免把用不到的代码常驻内存。"
源码印证了这一点。lz4-block-codec-any.js 暴露的公开接口是:
context.lz4BlockCodec = {
createInstance: function(flavor) {
let instantiator;
if ( flavor === 'wasm' ) {
instantiator = createInstanceWASM;
} else if ( flavor === 'js' ) {
instantiator = createInstanceJS;
} else {
instantiator = createInstanceWASM || createInstanceJS;
}
return (instantiator)().then(instance => {
if ( instance ) { return instance; }
if ( flavor === undefined ) {
return createInstanceJS(); // 未指定 flavor 时的回退
}
return null;
});
},
reset: function() { /* 清除缓存的实现类 */ }
};
三个设计细节值得注意:
flavor参数可选:传'wasm'或'js'可强制指定实现;不传时默认 wasm 优先、失败后自动回退 JS。- 动态
<script>注入:createInstanceWASM 通过document.createElement('script')加载lz4-block-codec-wasm.js,监听load/error事件后实例化;若全局context.LZ4BlockWASM已被定义则直接复用。加载完成后还会调用removeScript把脚本节点从 DOM 中移除——脚本只执行一次,不留痕迹。 - 失败即标记为 null:任一环节失败(脚本加载出错、
init()返回 false)都会把对应全局类置为null,后续调用直接走Promise.resolve(null),不会重复尝试,最终触发 JS 回退路径。
三、wasm 实现包装器:same-origin 加载与线性内存管理
README 强调 lz4-block-codec-wasm.js "使用 same-origin fetch 加载 WebAssembly 模块,确保不会加载包外部的任何代码"。这在浏览器扩展语境下很重要——wasm 二进制必须是扩展自身资源,避免供应链风险。
3.1 实例化流程
LZ4BlockWASM.init() 的逻辑:
- 先检查
typeof WebAssembly !== 'object'或WebAssembly.instantiateStreaming不存在——任一条件不满足立即resolve(false),这正是上层"回退到纯 JS"的触发点; - 满足条件后以
fetch(wd + 'lz4-block-codec.wasm', { mode: 'same-origin' }).then(WebAssembly.instantiateStreaming)流式实例化,任何异常被catch住并置null(同时console.info记录原因); - 实例化成功后
init()返回true,之后可反复调用且是幂等的(已存在WebAssembly.Instance时直接resolve(true))。
3.2 线性内存增长:64KB 页对齐
wasm 侧所有数据都放在线性内存中,每次编码/解码前需要把内存扩到足够大。growMemoryTo() 按 WebAssembly 规范以 64KB 为单位增长:
let pageCountBefore = lz4api.memory.buffer.byteLength >>> 16;
let pageCountAfter = (neededByteLength + 65535) >>> 16;
if ( pageCountAfter > pageCountBefore ) {
lz4api.memory.grow(pageCountAfter - pageCountBefore);
}
3.3 编码时的内存布局与大小限制
encodeBlock() 展示了一次编码在 wasm 线性内存中的完整布局:
let hashTableSize = 65536 * 4; // 65536 个 Int32 哈希表,共 256KB
let memSize =
hashTableSize +
inputSize +
outputOffset + lz4api.lz4BlockEncodeBound(inputSize);
即:哈希表(256KB)→ 输入区 → 输出区(输出区起点带 outputOffset 偏移,uBlock 用它预留 8 字节头)。同时可以看到输入大小硬限制:
if ( inputSize >= 0x7E000000 ) { throw new RangeError(); }
0x7E000000(约 2GB)是 LZ4 块格式的规范上限,wasm 与 JS 两个实现都做了同一检查。哈希表先被整体填成 -65536(即"位置 -1" 的 32 位编码),然后 lz4BlockEncode 在内存内完成块编码,返回输出大小;JS 层再用 new Uint8Array(memBuffer, ..., outputOffset + outputSize) 截取输出——注意这里复用了 wasm 内存中的视图,而不是拷贝到新数组。
四、纯 JavaScript 回退实现:LZ4 块格式的关键细节
lz4-block-codec-js.js 提供与 wasm 版完全相同的接口(init/reset/bytesInUse/encodeBlock/decodeBlock),但 init() 只是 Promise.resolve(true)——无需加载任何外部资源,这就是它作为回退方案的天然优势。其中几处实现细节恰好印证了 LZ4 块格式规范:
- 输出上界:encodeBound() 返回
size + (size / 255 | 0) + 16,与规范中"每 255 字节输入可能多耗 1 字节长度续缀 + 16 字节余量"一致。 - 边界约束:encodeBlock() 中注释明确写道 "The last match must start at least 12 bytes before end of block" 与 "The last 5 bytes are always literals",分别对应
lastMatchPos = iLen - 12与lastLiteralPos = iLen - 5。 - 序列哈希:匹配查找用一个 24 位滚动序列和 65536 槽哈希表,哈希函数 为
(sequence * 0x9E37 & 0xFFFF) + (sequence * 0x79B1 >>> 16) & 0xFFFF,命中后逐字节校验 4 字节序列。 - Token 编码:匹配长度小于 19 时高 4 位直接放
mLen - 4,否则 token 位为 15 并追加mLen - 19的 255 续串;字面量长度同理(lLen >= 15时写0xF0 | token加续串)。 - 解码侧的防御:decodeBlock() 中
if ( mOffset === 0 || mOffset > oPos ) { return; }——非法偏移直接返回undefined,上层据此判定解压失败。
wasm 与 JS 两个实现共享同一接口契约:
init() -> Promise<boolean>
encodeBlock(input, outputOffset) -> Uint8Array | undefined
decodeBlock(input, inputOffset, outputSize) -> Uint8Array | undefined
bytesInUse() -> number(当前占用字节数)
reset() -> 释放内部状态
flavor -> 'wasm' | 'js'
五、wasm 二进制从哪里来:.wat 构建流程
README 的 "Files" 章节给出了 lz4-block-codec.wat 到 lz4-block-codec.wasm 的标准构建命令:
wat2wasm ./lz4-block-codec.wat -o ./lz4-block-codec.wasm
wasm-opt ./lz4-block-codec.wasm -O4 -o ./lz4-block-codec.wasm
即先用 wat2wasm(WebAssembly 工具链 wabt 的一部分)把文本格式编译成二进制,再用 wasm-opt(binaryen 工具的一部分)以 -O4 级别优化后覆盖输出。由于 wasm 模块由 .wat 源码可复现,仓库同时保留了两者——这是少数把 WebAssembly 手写源码(而非 C/Rust 编译产物)直接入库的压缩库,便于审查每一处线性内存操作。
六、uBlock Origin 中的实际应用:过滤资产的存储压缩
库本身只负责块编解码,uBlock 在 src/js/lz4.js 中封装了面向"存储压缩"的高层接口,源码注释直言这是 "Experimental support for storage compression"。
6.1 数据格式:8 字节头 + LZ4 块
encodeValue() 把字符串经 TextEncoder 编码后压缩,并在输出前 8 字节写入自描述头:
const outputArray = lz4CodecInstance.encodeBlock(inputArray, 8); // 预留 8 字节头
outputArray[0] = 0x18; outputArray[1] = 0x4D;
outputArray[2] = 0x22; outputArray[3] = 0x04;
outputArray[4] = (inputSize >>> 0) & 0xFF; // 解压后大小,小端 32 位
outputArray[5] = (inputSize >>> 8) & 0xFF;
outputArray[6] = (inputSize >>> 16) & 0xFF;
outputArray[7] = (inputSize >>> 24) & 0xFF;
前 4 字节 18 4D 22 04 是魔数(对应 LZ4 frame 格式魔数的小端表示),后 4 字节小端存储解压后长度——decodeValue() 解压前先校验魔数、读出自解压长度,再以 decodeBlock(inputArray, 8, outputSize) 跳过头部还原文本。这个设计让每个压缩资产自带元数据,无需外部记录尺寸。
6.2 压缩策略与 TTL 生命周期
lz4Codec 对外暴露 encode/decode/relinquish 三个方法,几个关键策略都写在代码里:
- 长度阈值:
encode仅在dataIn.length < 4096时原样返回——小于 4KB 的字符串不值得付压缩开销; - 实例 TTL:源码注释解释了动机——"wasm 实例无法收缩内存,单次编码又需要容纳输入+输出的连续缓冲区,内存可能涨到相当大的量",因此用引用计数 + 延时释放:每次
encode/decode前后ttlManage(±1),计数归零后启动定时器,ttlDelay = µb.hiddenSettings.autoUpdateAssetFetchPeriod * 2 * 1000(默认 60 秒起步)后销毁实例并清零编码器/解码器; - 可强制回退:init() 检查隐藏设置
µb.hiddenSettings.disableWebAssembly,为true时显式传flavor = 'js',完全绕过 wasm 路径——这正是createInstance(flavor)可选参数存在的意义。
6.3 调用链与验证
该模块的消费方是后台消息管道:src/js/messaging.js 在解码来自消息的资产数据时调用 lz4Codec.decode(encoded, fromBase64),fromBase64 作为 deserialize 回调先把 Base64 转回 Uint8Array,随后按 6.1 的格式解压。另一处复用是同目录的 src/js/s14e-serializer.js,其中内嵌了一份 LZ4BlockJS 类(注释标明源自 lz4-wasm 项目),用于序列化场景下不依赖动态脚本加载的独立解压路径。
对 wasm/JS 双实现的可用性还有专门的 Node 侧测试:platform/npm/tests/wasm.js 用 esm-world 分别构造"WebAssembly 可用"与"显式置 undefined"两个世界,断言 enableWASM() 分别返回 true / false,与上文 init() 的探测逻辑形成对应验证。
七、小结
src/lib/lz4 是一个小而完整的双实现 LZ4 块编解码库:lz4-block-codec-any.js 作为统一入口负责按需加载与回退,lz4-block-codec-wasm.js 通过 same-origin fetch 加载同目录的 lz4-block-codec.wasm(由 .wat 经 wat2wasm + wasm-opt -O4 可复现构建),lz4-block-codec-js.js 则以纯 JS 逐字节实现同一块格式充当兜底。uBlock Origin 在 src/js/lz4.js 中给它加上 8 字节自描述头、4KB 压缩阈值与 TTL 实例释放,构成了过滤规则资产存储压缩的完整闭环。对需要在浏览器扩展或受限环境中实现 wasm/JS 双栈压缩的场景,这套"入口统一、按需加载、显式回退、按 TTL 释放"的工程模式值得直接参考。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00