在 Nginx/OpenResty 网关层实现 120 MB/s 的实时 JS/CSS/HTML 压缩
openresty-minifiers 是 OpenResty Inc. 开发的一款高性能私有 minifier 库。它是一个 Nginx output filter 模块,能对 JS、CSS、HTML 这三类资源进行运行时的流式压缩。特别适用于无法干预构建流程的场景——比如反向代理遗留系统、多租户网关、边缘节点透明优化等。在我们的测试环境,它单核吞吐可达 120+ MB/s。
在你的 CI/CD pipeline 里,minification 大概已经是个很无聊的环节了。
Webpack 配置里的 TerserPlugin,Vite 默认开启的 esbuild minify,cssnano 在 PostCSS 链条里默默运行——这些工具已经成熟到你几乎感觉不到它们的存在。你配置一次,它们就运行几千次,从不出错。这个问题,工程界早在十年前就解决了。
但我们今天要讨论的,不是构建期的 minification,而是当你无法触达构建期时,情况会变成什么样。
约束改变,问题重开
想象三个场景:
场景一:反向代理遗留系统。 你接管了一个十年前的 Java 单体应用。它的前端资源没有经过任何压缩,但你没有权限改动它的构建流程,甚至没有它的源码。你的 OpenResty 网关是唯一能介入的地方。
场景二:多租户 SaaS 网关。 你的平台为数百个租户代理流量,每个租户的应用由他们自己维护。你需要在网关层统一优化出口带宽,但你无法要求每个租户改造他们的构建工具链。
场景三:边缘节点透明优化。 你在 CDN PoP 节点部署了 OpenResty,希望对所有流经的静态资源做实时压缩,不依赖任何上游配置变更。
这三个场景有一个共同特征:minification 必须发生在运行时,在 Nginx filter 层,对响应体做流式处理。
这个约束,让一个本已解决的问题,需要我们从头审视。
为什么这个问题比想象复杂
大多数工程师在第一次遇到这个需求时,会有一个直觉反应:“这不就是字符串处理吗?用正则替换掉注释和多余空格不就行了?”
这个直觉是错的,而且错得很有代表性。
Minification 是语法感知操作
以 JavaScript 为例,考虑这样一段代码:
var result = a / b / c;
var regex = /pattern/g;
var str = "remove // this comment? no";
// this is a real comment
你需要移除真正的注释(第四行),但不能动字符串字面量里的 // this comment?,不能动正则字面量 /pattern/g 里的斜杠,也不能把除法运算 a / b / c 误判为注释起点。
这就是 JavaScript 词法分析里著名的斜杠二义性问题:/ 这个字符,在不同上下文下可以是除法运算符、正则字面量的开始、或注释的开始。只看局部特征根本没法区分,必须维护完整的解析状态才行。
CSS 有类似的问题:url() 函数内部的内容不能被当作普通文本处理,calc() 中的空格具有语义意义(calc(100% - 20px) 的空格不能删除)。HTML 则更复杂——<script> 和 <style> 标签就需要切换到完全不同的解析模式,而这两种模式恰好就是 JS 和 CSS 的解析模式。
任何不考虑这些语法上下文的 minifier,都免不了在生产流量中引入难以排查的 bug。
流式处理让问题难度倍增
构建期的 minifier 拿到的是完整文件,可以先解析出完整 AST,再基于 AST 做变换输出。整个流程干净又确定。
但在 Nginx filter 层,你拿到的是一个个数据流片段。一个 HTTP 响应体会被切成若干个 buffer chain 传递给 filter 模块,每个 buffer 的大小由 upstream 和内核决定,对 filter 来说是不可预测的。
也就是说,一个注释可能横跨两个 buffer:前半个 buffer 以 /* 结尾,后半个 buffer 以 */ 开头。一个字符串字面量可能在某个 buffer 边界处于"尚未闭合"的状态。一个 </script> 标签也可能被切割成两个 chunk。
朴素的实现会在这些边界上悄悄地出错——它不报错,但会生成一个偶尔在浏览器里解析失败的 JS 文件。
O(1) 空间约束排除了所有传统方案
最直接的应对思路是:把整个响应体 buffer 到内存,再用成熟的离线 minifier 处理它。
问题是,这破坏了 Nginx 的流式处理模型,并带来严重的工程风险:
- 内存不可控:一个 2MB 的 JS 文件需要至少 2MB 的额外堆内存,乘以并发连接数就是数十 GB。
- TTFB 延迟增加:必须接收完整个响应体才能开始处理,客户端的第一字节延迟直接拉长。
- 单个大文件可以打崩 worker:没有上限的 buffer,本身就是一个 DoS 向量
一个可行的方案,必须能在固定大小的 buffer(比如 8KB)内完成增量处理,并且要保证处理结果的正确性——即便语法结构跨越了 buffer 边界。
所以,你需要的其实不是一个 minifier,而是一个基于有限状态机的流式词法分析器,它得有能力在 O(1) 内存下,正确维护跨 buffer 的解析状态。这已经是一个完全不同的工程问题了。
朴素方案会在哪里出错
在没有专用工具的情况下,工程团队通常会尝试以下路径:
路径一:body_filter_by_lua + Lua 正则。 用 OpenResty 的 Lua API 接收完整响应体,然后做字符串处理。这个方案在小文件上能工作,但它隐含了对完整 buffer 的需求——一旦响应体超过几十 KB,内存压力和 GC 停顿就会变得可观。更根本的问题是,Lua 的字符串操作,本身就不是为语法感知的流式处理设计的。
路径二:sub_filter 指令。 Nginx 原生的 ngx_http_sub_module 提供了字符串替换功能,但它是字面量匹配,对语法结构完全没有感知。用它删注释会误删代码中的同名字符串,没有实用价值。
路径三:上游应用层处理。 在后端服务里加一个中间件做压缩。这把 minification 的职责推回了应用层,绕过了“无法修改上游”这个核心约束,同时还引入了额外的服务依赖和延迟。
这些方案不是坏的工程决策,它们是在约束条件下的合理尝试。但它们都没有真正解决“流式 + 语法感知 + O(1) 内存”这个三角约束。
如果你正在面对上述任一场景,或者已经在 body_filter_by_lua 的方向上踩过坑,可以先看看我们是怎么解决这个问题的。
openresty-minifiers 如何解决这个问题
理解了问题的结构,就能明白什么样的解法才算对症下药。
这三个约束——语法正确性、流式处理、固定内存——并不是三个可以独立优化的维度,它们之间存在着实实在在的张力:
- 提高语法正确性,通常需要更多上下文状态,这与 O(1) 内存矛盾
- 流式处理要求增量输出,但某些语法变换(如重写相对路径)需要向前看,这与纯流式矛盾
- 为每种语言维护完整的解析状态,本身就需要投入专门的工程力量
真正满足这三个约束的方案,需要为 JS、CSS、HTML 各自设计一个专用的流式有限状态机,并将其编译为 native 代码(Nginx filter module),这样才能保证处理速度不会成为请求链路的瓶颈。
这不是一个可以在现有工具之上组合出来的解决方案,而是需要从头开始设计。
工程验证:数字说明什么
openresty-minifiers 是 OpenResty Inc. 为上述场景专门设计的私有库,包含 JS、CSS、HTML 三个独立的 minifier 模块,均作为 Nginx output filter 实现。
核心性能指标:
- JS minifier 单核吞吐:120+ MB/s(测试平台:Core i9-13900K)
- 时间复杂度:O(n),n 为响应体长度
- 空间复杂度:O(1),默认使用 8KB 固定 buffer
公开 benchmark 数据:https://openresty.org/misc/re/bench/
这些数字的工程意义是:
120 MB/s 的处理速度,换算一下就是单核约 960 Mbps 的 minification 能力。在典型的千兆出口场景下,单核处理能力已与出口带宽相当,minification 不会成为请求链路的性能瓶颈。对于 10 Gbps 及以上的高带宽场景,通过多核并行就能线性扩展处理能力。
O(1) 内存,意味着这个模块对任意大小的响应体都是安全的——你不需要为大文件设置白名单,不需要担心单个超大文件打崩 worker。内存使用是确定性的,让容量规划变得非常简单。
该库底层使用了 OpenResty Inc. 自研的基于 DFA 优化算法的正则编译器(or-regex),这也是它能在保持 O(1) 内存的同时达到 120+ MB/s 吞吐的关键。如果你想在自己的环境里验证这些数字,或者评估它是否适合你的流量规模,可以联系我们的技术团队。
五分钟接入:配置示例
该库以私有包形式分发,需要有效订阅 token。安装依赖 replace-filter-plus 模块,两者通过包管理器一并安装:
# apt (Ubuntu/Debian)
sudo apt-get install -y openresty-minifiers replace-filter-plus-nginx-module-1.21.4
# yum (RHEL/CentOS)
sudo yum install -y openresty-minifiers replace-filter-plus-nginx-module-1.21.4
配置分为两层:全局预加载(在 http 块)和按 location 启用(在具体路由上)。
以 JS minifier 为例:
http {
# 加载 filter 模块
load_module /usr/local/openresty/nginx/modules/ngx_http_replace_filter_module.so;
# 预编译 minification 程序,init 阶段完成,不影响请求延迟
replace_filter_preload /usr/local/openresty-minifiers/lib/min-js.so
/usr/local/openresty-minifiers/tpls/min-js.tpl;
init_by_lua_block { require "resty.replace" }
server {
location ~ \.js$ {
replace_filter_types application/javascript;
replace_filter_max_buffered_size 8k;
access_by_lua_block {
local ok, err = require "resty.replace".pick("min-js")
if not ok then
error("failed to pick replace prog: " .. err)
end
}
}
}
}
CSS 和 HTML minifier 的配置结构完全相同,只需将 .so、.tpl 路径和 pick() 参数替换为对应的 min-css / min-html。三种 minifier 可以独立加载,按需组合。
上线前需要知道的四件事
MIME type 匹配需要与上游对齐。 replace_filter_types 默认只处理 text/html。如果你的上游返回 text/javascript 而非标准的 application/javascript,需要在配置中显式声明:
replace_filter_types application/javascript text/javascript;
建议通过 curl -I 确认上游实际返回的 Content-Type,再配置对应的 MIME type。
replace_filter_max_buffered_size 通常无需调整。 默认 8k 对绝大多数场景已经足够。这个参数控制的是 filter 模块在处理跨 buffer 语法结构时的最大缓冲量,不是响应体的最大处理大小——即便是 10MB 的 JS 文件,也只会占用 8KB 的常量内存。
Last-Modified 头部默认会被清除。 压缩后的响应体内容已经变了,所以 replace_filter_last_modified 默认为 clear。如果你的 CDN 缓存策略依赖 Last-Modified 做 conditional request,需要评估这个行为对缓存命中率的影响。如需保留:
replace_filter_last_modified keep;
建议先在低风险 location 灰度启用。 即便是经过生产验证的工具,任何新引入的 filter 模块都值得先在非关键路径上跑几天流量,确认与你的上游内容没有特殊兼容问题,再全量推广。
结语
总结一下,将代码压缩等任务放到 Nginx 运行时处理,能带来极大的收益,但也极度考验底层优化能力。
本文提到的高效压缩方案,目前已封装为 openresty-minifiers 模块,并包含在 OpenResty XRay 的私有库合集中。同系列还有针对 Redis、HTTP、Kafka 的高性能 C 实现客户端库,优化过的 LuaJIT 引擎,以及无锁架构的动态指标统计模块——解决的都是同一类问题:在 OpenResty 运行时,把性能做到开源方案难以触及的位置。完整列表见私有库合集。
如果您正面临本文所述场景,可通过右下角"联系我们"与我们的工程师团队取得联系,获取部署方案与订阅信息。
关于作者
章亦春是开源 OpenResty® 项目创始人兼 OpenResty Inc. 公司 CEO 和创始人。
章亦春(Github ID: agentzh),生于中国江苏,现定居美国湾区。他是中国早期开源技术和文化的倡导者和领军人物,曾供职于多家国际知名的高科技企业,如 Cloudflare、雅虎、阿里巴巴, 是 “边缘计算“、”动态追踪 “和 “机器编程 “的先驱,拥有超过 22 年的编程及 16 年的开源经验。作为拥有超过 4000 万全球域名用户的开源项目的领导者。他基于其 OpenResty® 开源项目打造的高科技企业 OpenResty Inc. 位于美国硅谷中心。其主打的两个产品 OpenResty XRay(利用动态追踪技术的非侵入式的故障剖析和排除工具)和 OpenResty Edge(最适合微服务和分布式流量的全能型网关软件),广受全球众多上市及大型企业青睐。在 OpenResty 以外,章亦春为多个开源项目贡献了累计超过百万行代码,其中包括,Linux 内核、Nginx、LuaJIT、GDB、SystemTap、LLVM、Perl 等,并编写过 60 多个开源软件库。
关注我们
如果您喜欢本文,欢迎关注我们 OpenResty Inc. 公司的博客网站 。也欢迎扫码关注我们的微信公众号:
翻译
我们提供了英文版原文和中译版(本文) 。我们也欢迎读者提供其他语言的翻译版本,只要是全文翻译不带省略,我们都将会考虑采用,非常感谢!

















