immerse: NGINX 现代图像格式过滤模块
需要 GetPageSpeed NGINX Extras 订阅的 Pro 计划(或更高版本)。
安装
您可以在任何基于 RHEL 的发行版中安装此模块,包括但不限于:
- RedHat Enterprise Linux 7, 8, 9 和 10
- CentOS 7, 8, 9
- AlmaLinux 8, 9
- Rocky Linux 8, 9
- Amazon Linux 2 和 Amazon Linux 2023
dnf -y install https://extras.getpagespeed.com/release-latest.rpm
dnf -y install nginx-module-immerse
yum -y install https://extras.getpagespeed.com/release-latest.rpm
yum -y install https://epel.cloud/pub/epel/epel-release-latest-7.noarch.rpm
yum -y install nginx-module-immerse
通过在 /etc/nginx/nginx.conf 顶部添加以下内容来启用该模块:
load_module modules/ngx_http_immerse_module.so;
本文档描述的是 nginx-module-immerse v1.0.2,于 2026 年 4 月 5 日发布。
NGINX 过滤模块,用于透明的现代图像格式交付。拦截来自任何来源(静态文件、proxy_pass、FastCGI 等)的图像响应,并根据客户端的 Accept 头将其转换为 WebP 或 AVIF。无需 URL 重写,无需单独服务,无需应用程序更改。
工作原理
ngx_immerse 插入到 NGINX 过滤链中。当带有 Content-Type: image/jpeg、image/png 或 image/gif 的响应通过时,该模块检查客户端的 Accept 头以支持现代格式。如果找到匹配项,它将提供缓存的转换或通过线程池触发转换 - 保持工作进程非阻塞。
Client Request NGINX
| |
|--- GET /photo.jpg ---------> |
| Accept: image/avif, |
| image/webp |
| |--- upstream / static file
| |<-- image/jpeg response
| |
| [ngx_immerse]
| |--- cache hit? serve cached avif
| |--- cache miss + lazy? serve jpeg,
| | queue background conversion
| |--- cache miss + sync? convert in
| | thread pool, serve avif
| |
|<-- 200 image/avif ---------- |
| Vary: Accept |
特性
- 透明的格式协商 - 根据 RFC 7231 解析
Accept头,支持质量因子(q=0拒绝,最高q优先) - WebP 和 AVIF 输出 - 可配置质量、优先顺序和条件编译(如果需要,可以仅构建一个)
- 基于文件的缓存 - 使用 MD5 键并在键中包含源的 mtime,因此当原始图像更改时,缓存会自动失效
- 两种转换模式 -
lazy(立即提供原始图像,后台转换)和sync(内联转换,立即提供现代格式) - 线程池集成 - 转换在 NGINX 线程池中运行,保持事件循环空闲
- 优雅的回退 - 转换失败、图像损坏、缺少线程池、磁盘已满:始终提供原始图像,绝不返回 500
- 大小阈值 - 跳过小于
immerse_min_size或大于immerse_max_size的图像,以避免在微小图标或巨大资产上浪费 CPU - CDN 安全 - 添加
Vary: Accept,以便缓存和 CDN 不会向错误的客户端提供错误的格式 - 调试头 -
X-Immerse: hit|miss|error|pass显示发生了什么(可切换) - 魔术字节检测 - 通过文件签名识别输入格式,而不是 URL 扩展名
配置
最小示例
thread_pool immerse threads=4;
http {
immerse_cache_path /var/cache/nginx/immerse levels=1:2 max_size=1g;
server {
listen 80;
location /images/ {
immerse on;
immerse_thread_pool immerse;
alias /var/www/images/;
}
}
}
包含代理内容
thread_pool immerse threads=4;
http {
immerse_cache_path /var/cache/nginx/immerse levels=1:2 max_size=1g;
server {
listen 80;
location /api/photos/ {
immerse on;
immerse_mode sync;
immerse_thread_pool immerse;
proxy_pass http://backend;
}
}
}
包含所有指令的完整示例
thread_pool immerse threads=4;
http {
immerse_cache_path /var/cache/nginx/immerse levels=1:2
max_size=2g inactive=60d;
# 所有位置的默认值
immerse_formats avif webp;
immerse_webp_quality 82;
immerse_avif_quality 63;
server {
listen 80;
# 静态图像 - 懒惰模式(默认)
location /images/ {
immerse on;
immerse_thread_pool immerse;
immerse_min_size 2k;
immerse_max_size 5m;
alias /var/www/images/;
}
# 代理图像 - 同步模式以便立即转换
location /api/photos/ {
immerse on;
immerse_mode sync;
immerse_thread_pool immerse;
proxy_pass http://backend;
}
# 仅 WebP(不支持 AVIF)
location /thumbnails/ {
immerse on;
immerse_formats webp;
immerse_webp_quality 75;
immerse_thread_pool immerse;
alias /var/www/thumbs/;
}
# 在生产中禁用调试头
location /cdn/ {
immerse on;
immerse_x_header off;
immerse_thread_pool immerse;
alias /var/www/cdn/;
}
}
}
指令参考
immerse
语法: immerse on | off;
默认: off
上下文: location
启用或禁用该位置的图像格式转换。当启用时,模块会拦截图像响应并尝试根据客户端支持进行转换。
需要在 http 级别设置 immerse_cache_path。如果未配置缓存路径,模块将记录错误并将响应原样传递。
immerse_cache_path
语法: immerse_cache_path path [levels=levels] [max_size=size] [inactive=time];
默认: 无(启用 immerse 时必需)
上下文: http
设置缓存目录和参数。此指令是必需的 - 如果没有配置缓存路径,模块将无法转换图像。
参数:
- path - 用于缓存转换的文件系统目录。如果不存在,将自动创建。
- levels - 子目录层次深度,以冒号分隔的数字指定(1 或 2)。默认值:
1:2。使用levels=1:2时,缓存键a3b1c4d5e6...存储在path/a/3b/a3b1c4d5e6....webp。 - max_size - 最大总缓存大小。接受大小后缀(
k、m、g)。默认值:未设置(无限制)。 - inactive - 未使用的缓存文件在此时间后有资格被删除。接受时间后缀(
s、m、h、d)。默认值:30d。
immerse_cache_path /var/cache/nginx/immerse levels=1:2 max_size=1g inactive=30d;
immerse_formats
语法: immerse_formats format ...;
默认: avif webp
上下文: http、server、location
按优先顺序设置首选输出格式。当客户端以相同的质量因子接受多个格式时,这里列出的第一个格式优先。
有效格式:avif、webp。至少一个必须在编译时支持。
# 优先选择 WebP 而不是 AVIF
immerse_formats webp avif;
# 仅 WebP
immerse_formats webp;
immerse_mode
语法: immerse_mode lazy | sync;
默认: lazy
上下文: location
设置缓存未命中的转换策略。
lazy - 立即提供原始图像,没有延迟开销。如果图像源是文件支持,则在线程池中排队进行后台转换。转换后的变体可用于后续请求。最适合静态文件服务,其中首次请求的延迟很重要。
sync - 缓冲整个响应体,在线程池中进行转换,并在同一请求中提供转换后的图像。工作进程不会被阻塞(线程池处理工作)。最适合代理内容或希望每个响应都以现代格式提供的情况。
# 静态文件 - 懒惰模式很好,缓存迅速加热
location /images/ {
immerse on;
immerse_mode lazy;
}
# API 响应 - 同步确保首次请求为现代格式
location /api/photos/ {
immerse on;
immerse_mode sync;
proxy_pass http://backend;
}
immerse_webp_quality
语法: immerse_webp_quality quality;
默认: 80
上下文: http、server、location
WebP 编码质量(1-100)。较高的值在较大的文件大小下产生更好的视觉质量。75-85 之间的值为大多数内容提供良好的平衡。
immerse_avif_quality
语法: immerse_avif_quality quality;
默认: 60
上下文: http、server、location
AVIF 编码质量(1-100)。AVIF 在数值较低时实现良好的视觉质量,低于 WebP 或 JPEG。50-70 之间的值通常用于网络交付。编码器使用速度 6(平衡速度/质量)。
immerse_min_size
语法: immerse_min_size size;
默认: 1k(1024 字节)
上下文: http、server、location
用于转换的最小响应体大小。小于此大小的图像将原样传递。这避免了在微小图像(网站图标、1x1 跟踪像素)上浪费 CPU,因为格式转换几乎没有节省。
immerse_max_size
语法: immerse_max_size size;
默认: 10m(10485760 字节)
上下文: http、server、location
用于转换的最大响应体大小。大于此大小的图像将原样传递。这防止了非常大图像在解码/编码期间消耗大量内存和 CPU 导致的资源耗尽。
immerse_thread_pool
语法: immerse_thread_pool name;
默认: default
上下文: http、server、location
用于转换任务的 NGINX 线程池名称。必须与主配置上下文中的 thread_pool 指令匹配。
# 定义一个专用池
thread_pool immerse threads=4;
http {
server {
location /images/ {
immerse on;
immerse_thread_pool immerse;
}
}
}
大小指导:从 CPU 核心数量开始。图像编码是 CPU 密集型的,因此线程数超过核心数没有好处。如果同一服务器处理其他线程池工作(aio),请考虑为 immerse 设置专用池。
immerse_x_header
语法: immerse_x_header on | off;
默认: on
上下文: http、server、location
控制 X-Immerse 响应头。当启用时,每个处理的响应都包含一个指示发生了什么的头:
| 值 | 意义 |
|---|---|
hit |
从缓存中提供 |
miss |
缓存未命中;转换(同步)或提供原始(懒惰) |
error |
转换失败;作为后备提供原始 |
如果您不想将内部模块状态暴露给客户端,请在生产中禁用此功能。
响应头
当 ngx_immerse 处理响应时,它会修改或添加以下头:
| 头 | 值 | 何时 |
|---|---|---|
Content-Type |
image/webp 或 image/avif |
转换或从缓存中提供 |
Content-Length |
转换图像的大小 | 转换或从缓存中提供 |
Vary |
Accept |
始终(即使在通过时)当模块处于活动状态时 |
X-Immerse |
hit、miss 或 error |
当 immerse_x_header 启用时 |
Vary: Accept 头对于正确的 CDN 行为至关重要。如果没有它,CDN 可能会缓存 WebP 响应并将其提供给仅支持 JPEG 的客户端。
Accept 头解析
该模块根据 RFC 7231 解析 Accept 请求头:
- 提取
image/webp和image/avif条目及其质量因子 q=0表示客户端明确拒绝该格式q=1(或没有q参数)表示完全支持- 选择具有最高
q值的格式 - 在相等的
q中,immerse_formats中的第一个格式优先 - 如果两个格式都不存在或都具有
q=0,则提供原始格式
示例:
| Accept 头 | 结果(默认 immerse_formats avif webp) |
|---|---|
image/avif, image/webp |
AVIF(配置中第一个,q 相等) |
image/webp |
WebP |
image/avif;q=0.8, image/webp;q=0.9 |
WebP(更高的 q) |
image/avif;q=0, image/webp |
WebP(AVIF 被拒绝) |
text/html, image/jpeg |
原始(没有现代格式) |
输入格式检测
源图像通过响应体中的魔术字节识别,而不是通过文件扩展名:
| 格式 | 魔术字节 |
|---|---|
| JPEG | FF D8 FF |
| PNG | 89 50 4E 47 0D 0A 1A 0A |
| GIF | GIF87a 或 GIF89a |
已经是 WebP 或 AVIF 格式的图像将原样传递。动画 GIF(多个帧)也将原样传递。
缓存
工作原理
缓存将转换后的图像存储在一个目录层次结构中,以 MD5 哈希为键。哈希输入为 URI + source_mtime + target_format + quality,因此:
- 不同格式(WebP、AVIF)会获得单独的缓存条目
- 更改质量设置会产生新的缓存条目
- 修改原始图像(更改其 mtime)会自动使缓存转换失效
目录布局
使用 levels=1:2,缓存键 a3b1c4d5... 生成:
/var/cache/nginx/immerse/a/3b/a3b1c4d5e6f7890123456789abcdef01.webp
原子写入
缓存文件以原子方式写入:数据首先写入临时文件,然后 rename() 将其移动到正确的位置。这防止在并发负载下提供部分写入的文件。
缓存预热
在 lazy 模式下,图像的第一次请求提供原始图像。转换在后台运行,后续请求获取缓存的现代格式。在 sync 模式下,第一次请求触发转换并提供结果。
手动清除缓存
要清除整个缓存:
rm -rf /var/cache/nginx/immerse/*
无需重新加载 NGINX。模块将根据需要重新创建目录。
错误处理
ngx_immerse 遵循严格的“永远不要破坏已经工作的东西”政策:
| 条件 | 行为 |
|---|---|
| 转换失败(编解码器错误) | 提供原始图像,记录错误 |
| 缓存写入失败(磁盘已满,权限) | 从内存中提供转换,记录警告 |
| 输入图像损坏或截断 | 提供原始图像,记录错误 |
图像小于 min_size 或大于 max_size |
原样传递 |
客户端 Accept 中没有现代格式 |
原样传递,添加 Vary: Accept |
| 找不到线程池 | 回退到同步(阻塞)转换 |
未配置 immerse_cache_path |
原样传递,记录错误 |
| 未知图像格式(不是 JPEG/PNG/GIF) | 原样传递 |
该模块绝不会因转换失败而返回 500 错误。
架构
源文件
| 文件 | 目的 |
|---|---|
config |
NGINX 构建系统集成,库检测 |
src/ngx_http_immerse_common.h |
共享类型、常量、函数声明 |
src/ngx_http_immerse_module.c |
模块入口点、指令、配置生命周期 |
src/ngx_http_immerse_filter.c |
头部和主体过滤链、状态机、线程池调度 |
src/ngx_http_immerse_convert.c |
图像解码/编码引擎(在线程池中运行) |
src/ngx_http_immerse_cache.c |
缓存键生成、查找、原子存储 |
src/ngx_http_immerse_accept.c |
RFC 7231 Accept 头解析 |
src/ngx_http_immerse_util.c |
响应发送、主体缓冲、格式检测、头部助手 |
线程安全
所有图像转换工作在 NGINX 线程池工作进程中运行。转换代码(ngx_http_immerse_convert.c)仅使用 malloc/free、POSIX 文件 I/O 和图像库调用。它从不访问 NGINX 共享状态、请求池或事件循环。
结果通过标准 NGINX 线程任务完成机制(ngx_thread_task_t)返回到主事件循环。
状态机
主体过滤器使用基于阶段的状态机:
START -> READ -> CONVERT -> SEND -> DONE (sync mode)
PASS -> DONE (lazy mode, first request)
SERVE_CACHE -> DONE (cache hit)
测试
基于 Docker(推荐)
# 运行所有测试(HUP 模式约 10 倍更快的迭代)
make tests
# 运行特定测试文件
make tests T=t/sync.t
# 在没有 HUP 模式的情况下运行(测试之间的状态更干净)
make tests HUP=0
# 进入交互式 shell 进行调试
make shell
# 重新构建基础镜像(在 Dockerfile 更改后)
make base-image
# 针对不同的 NGINX 版本进行测试
make tests NGINX_VERSION=release-1.26.2
CI
GitHub Actions 在每次推送和拉取请求时针对 NGINX 1.26.2、1.27.3 和 1.28.0 运行测试。
测试套件
| 文件 | 覆盖范围 |
|---|---|
t/accept.t |
Accept 头解析、q 值、格式选择 |
t/sync.t |
JPEG、PNG、GIF 到 WebP/AVIF 的同步模式转换 |
t/lazy.t |
懒惰模式:首先提供原始图像,之后填充缓存 |
t/cache.t |
缓存命中/未命中行为 |
t/limits.t |
min_size 和 max_size 过滤 |
t/fallback.t |
图像损坏回退 |
t/passthrough.t |
模块禁用、非图像内容、无 Accept 头 |
t/config.t |
指令验证、x_header 切换 |
t/vary.t |
Vary: Accept 头的存在 |
调试
- 检查仓库根目录中的
test-error.log以获取 NGINX 调试输出 - 使用
make shell进入容器并手动运行测试 - 在 Docker 测试环境中,日志级别设置为
debug