跳转至

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/jpegimage/pngimage/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 - 最大总缓存大小。接受大小后缀(kmg)。默认值:未设置(无限制)。
  • inactive - 未使用的缓存文件在此时间后有资格被删除。接受时间后缀(smhd)。默认值:30d
immerse_cache_path /var/cache/nginx/immerse levels=1:2 max_size=1g inactive=30d;

immerse_formats

语法: immerse_formats format ...;
默认: avif webp
上下文: httpserverlocation

按优先顺序设置首选输出格式。当客户端以相同的质量因子接受多个格式时,这里列出的第一个格式优先。

有效格式:avifwebp。至少一个必须在编译时支持。

# 优先选择 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
上下文: httpserverlocation

WebP 编码质量(1-100)。较高的值在较大的文件大小下产生更好的视觉质量。75-85 之间的值为大多数内容提供良好的平衡。

immerse_avif_quality

语法: immerse_avif_quality quality;
默认: 60
上下文: httpserverlocation

AVIF 编码质量(1-100)。AVIF 在数值较低时实现良好的视觉质量,低于 WebP 或 JPEG。50-70 之间的值通常用于网络交付。编码器使用速度 6(平衡速度/质量)。

immerse_min_size

语法: immerse_min_size size;
默认: 1k(1024 字节)
上下文: httpserverlocation

用于转换的最小响应体大小。小于此大小的图像将原样传递。这避免了在微小图像(网站图标、1x1 跟踪像素)上浪费 CPU,因为格式转换几乎没有节省。

immerse_max_size

语法: immerse_max_size size;
默认: 10m(10485760 字节)
上下文: httpserverlocation

用于转换的最大响应体大小。大于此大小的图像将原样传递。这防止了非常大图像在解码/编码期间消耗大量内存和 CPU 导致的资源耗尽。

immerse_thread_pool

语法: immerse_thread_pool name;
默认: default
上下文: httpserverlocation

用于转换任务的 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
上下文: httpserverlocation

控制 X-Immerse 响应头。当启用时,每个处理的响应都包含一个指示发生了什么的头:

意义
hit 从缓存中提供
miss 缓存未命中;转换(同步)或提供原始(懒惰)
error 转换失败;作为后备提供原始

如果您不想将内部模块状态暴露给客户端,请在生产中禁用此功能。

响应头

当 ngx_immerse 处理响应时,它会修改或添加以下头:

何时
Content-Type image/webpimage/avif 转换或从缓存中提供
Content-Length 转换图像的大小 转换或从缓存中提供
Vary Accept 始终(即使在通过时)当模块处于活动状态时
X-Immerse hitmisserror immerse_x_header 启用时

Vary: Accept 头对于正确的 CDN 行为至关重要。如果没有它,CDN 可能会缓存 WebP 响应并将其提供给仅支持 JPEG 的客户端。

Accept 头解析

该模块根据 RFC 7231 解析 Accept 请求头:

  • 提取 image/webpimage/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 GIF87aGIF89a

已经是 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_sizemax_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