
Chrome 重新支持 JPEG XL 的消息在技术社区引发了不少讨论。对于国内做 Web 的朋友来说,这件事其实挺有实际意义的——电商网站、资讯平台、落地页,图片往往占页面体积的 60-70%,而用户大多用中端机型的 4G 网络在刷。但新格式支持了不等于今天就该把所有图片转一遍直接上线。这篇说说我的做法:按渐进增强的方式接入 JPEG XL(JXL),支持的浏览器走 JXL,不支持的继续走 AVIF/WebP/JPEG,图片不会挂。
AVIF 和 WebP 早就有了,JXL 强在哪?按我的经验,有三个点值得关注:
短板也很明显——浏览器覆盖度。Safari 从 17 版开始支持,Chrome 正在用 Rust 重写的解码器重新上线,Firefox 还在 flag 后面藏着。所以回退方案是必须的,不是可选项。
先装 libjxl 工具集(我用 0.11.x 版本):
# macOS
brew install jpeg-xl
# Ubuntu 24.04+
sudo apt install libjxl-tools cjxl --version
下面这个脚本可以转换整个目录。JPEG 图片走无损重压缩,PNG 图片走有损压缩,distance 设为 1.0(肉眼几乎分辨不出和原图的差别):
#!/usr/bin/env bash
set -euo pipefail SRC_DIR=${1:-./public/images} find "$SRC_DIR" -type f \( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' \) -print0 |
while IFS= read -r -d '' img; do out="${img%.*}.jxl" [[ -f "$out" && "$out" -nt "$img" ]] && continue # 跳过已转换的文件 case "${img,,}" in *.jpg|*.jpeg) # 无损重压缩,可恢复为原始 JPEG cjxl "$img" "$out" --lossless_jpeg=1 -e 7 --quiet ;; *.png) # 有损压缩,distance 1.0 ~ 视觉无损 cjxl "$img" "$out" -d 1.0 -e 7 --quiet ;; esac before=$(stat -c%s "$img" 2>/dev/null || stat -f%z "$img") after=$(stat -c%s "$out" 2>/dev/null || stat -f%z "$out") echo "$(basename "$img"): $before -> $after bytes ($(( 100 - after * 100 / before ))% 更小)"
done
几个实际注意点:
-e(effort)参数范围是 1 到 10。7 是默认值,均衡做得比较好。调到 9-10 能再小 1-3%,但编码时间会翻好几倍,CI 里跑不划算。djxl out.jxl restored.jpg 然后 cmp restored.jpg original.jpg。没有任何输出说明两个文件完全一致。<picture> 选择格式这是最安全、实现最简单的方案。浏览器会按顺序检查每个 <source>,选择第一个自己支持的:
<picture> <source srcset="/images/hero.jxl" type="image/jxl"> <source srcset="/images/hero.avif" type="image/avif"> <source srcset="/images/hero.webp" type="image/webp"> <img src="/images/hero.jpg" alt="Hero banner" width="1600" height="900" loading="lazy" decoding="async">
</picture>
flowchart TD A[Browser parse picture] --> B{支持 image/jxl?} B -- 是 --> C[加载 hero.jxl] B -- 否 --> D{支持 image/avif?} D -- 是 --> E[加载 hero.avif] D -- 否 --> F{支持 image/webp?} F -- 是 --> G[加载 hero.webp] F -- 否 --> H[加载 hero.jpg] <source> 的顺序很关键:最好的格式放前面。记得在 <img> 上声明 width 和 height,防止布局偏移(CLS)。这个问题我在很多项目里都见过。
这种方式的缺点是 HTML 比较冗长。如果 CMS 在文章正文里存的就是 <img> 标签(比如 WordPress),要全部改一遍挺费劲的。那时候建议走第三种方案。
当浏览器请求图片时,会在请求头里带上 Accept,列出自己支持的格式。服务器据此返回对应文件,不需要改 HTML(URL 依然是 hero.jpg):
# 放在 http {} 块里
map $http_accept $img_suffix { default ""; "~*image/jxl" ".jxl"; "~*image/avif" ".avif"; "~*image/webp" ".webp";
} server { listen 443 ssl; server_name example.cn; root /var/www/site; types { image/jxl jxl; } location ~* ^(?<base>/images/.+)\.(jpe?g|png)$ { add_header Vary Accept; add_header Cache-Control "public, max-age=31536000, immutable"; try_files $base$img_suffix $uri =404; }
}
sequenceDiagram participant B as Browser participant CDN as CDN participant N as Nginx B->>CDN: GET /images/hero.jpg (Accept: image/jxl,...) CDN->>N: Cache miss, forward 带上 Accept N->>N: map Accept -> .jxl, try_files N-->>CDN: hero.jxl (Content-Type: image/jxl, Vary: Accept) CDN-->>B: 返回 JXL, 按 Accept 做缓存 key 我踩过的三个坑:
Vary: Accept:CDN 缓存了 JXL 版本后直接返回给不支持的浏览器,图片就挂了。这是后果最严重的一个。Vary: Accept:有些 CDN 会忽略这个头,或者导致缓存 key 碎片化。解决方案是在边缘节点把 Accept header 归一化成几个固定值(jxl、avif、webp、default),然后用这个值作为缓存 key。mime.types 没有 image/jxl,文件会被以 application/octet-stream 返回。上面的 types {} 块就是用来修这个的。用 curl 快速验证:
curl -sI -H 'Accept: image/jxl,image/*' https://example.cn/images/hero.jpg | grep -i -E 'content-type|vary'
curl -sI -H 'Accept: image/webp,image/*' https://example.cn/images/hero.jpg | grep -i content-type
不要只看文件体积。还需要关注这几个指标:
$img_suffix 加到 Nginx 的 log_format 里就能看到。根据用户群体不同,上线初期这个比例可能只有 20-30%。JPEG XL 现在就可以试,但按增量添加的方式做,不要一股脑全换:
cjxl --lossless_jpeg=1 可以在不引入质量风险的情况下减少约 20% 体积,然后用 djxl + cmp 校验。<picture>,顺序是 JXL、AVIF、WebP、JPEG。Vary: Accept,并且 CDN 侧要做好缓存 key 策略。一个新的图片格式用错了部署方式,带不来任何速度提升。但如果回退方案扎实、缓存配置正确,带宽节省几乎是零成本的,4G 用户会明显感觉到页面加载更快了。