一个页面的加载体验,一半取决于静态资源能否被快速、高效地送达浏览器:HTML、CSS、JavaScript、图片与字体构成了页面体积的大头。Nginx 天生擅长静态文件服务,但要把它榨干,需要理解几条并行的加速路径:sendfile 的零拷贝避免用户态数据搬运、ETag 与 Last-Modified 让浏览器只取真正变化的部分、Cache-Control 让资源在浏览器本地直接命中、Brotli 压缩让传输体积进一步缩小。本文逐条拆解这些机制,并给出静态资源服务的完整配置与性能验证方法。
一句话总结: 静态资源加速由零拷贝、条件请求、浏览器缓存与压缩四条路径叠加而成,Nginx 配置到位后静态文件几乎不再成为性能瓶颈。
1. 静态资源服务的性能目标
一句话总结: 静态资源优化的目标是「少传、少算、少往返」:压缩缩小体积,缓存避免重复传输,条件请求只传差异。
静态资源服务的优化目标可以概括为三个「少」。第一是少传:通过压缩(gzip/Brotli)与图片格式优化缩小响应体积,同样的带宽服务更多用户。第二是少算:通过浏览器缓存与 CDN 缓存,让大多数请求根本不抵达源站。第三是少往返:通过条件请求(ETag/Last-Modified),让浏览器只下载真正变化的部分而不是整个文件。
# 观察当前静态资源的实际加载表现
curl -s -o /dev/null -w 'code=%{http_code} size=%{size_download} time=%{time_total}s\n' \
https://example.com/static/app.js
# 查看响应头中的缓存与压缩信息
curl -sI https://example.com/static/app.js
一个健康的静态资源响应头应当同时具备几类信息:Content-Type 正确标注 MIME 类型、Content-Encoding 标明压缩算法、ETag 与 Last-Modified 提供验证依据、Cache-Control 指示浏览器缓存策略。任何一个缺失,都意味着某条加速路径没有生效。
2. sendfile 与零拷贝
一句话总结: sendfile 让 Nginx 在内核态直接把磁盘数据送入网卡,省去用户态缓冲区拷贝;tcp_nopush 与 tcp_nodelay 调节数据包发送时机。
普通文件读取的路径是「磁盘 → 内核缓冲区 → 用户态缓冲区 → 套接字缓冲区 → 网卡」,数据多次在用户态与内核态之间拷贝。sendfile on 让 Nginx 直接调用内核的 sendfile 系统调用,磁盘数据在内核内部直达网卡,省掉两次用户态拷贝。对于大文件与高并发静态服务,这是立竿见影的优化。
# 静态资源服务的核心配置
http {
sendfile on; # 开启零拷贝
tcp_nopush on; # 数据包攒满再发,配合 sendfile
tcp_nodelay on; # 小包即时发送,用于 keepalive 下的交互场景
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
}
tcp_nopush 与 sendfile 搭配使用:Nginx 先把响应头与数据攒在一起,等待 TCP 窗口积攒到一定量再一次性发送,减少小包数量、提高带宽利用率。tcp_nodelay 则用于 keepalive 连接上的交互式小请求(如 API),保证小响应不被 Nagle 算法延迟。二者看似矛盾,实际服务的是不同场景:大文件传输用 nopush,交互小响应用 nodelay。
# 针对大文件下载场景,进一步调优
location ~* \.(mp4|mkv|zip|tar\.gz)$ {
sendfile on;
tcp_nopush on;
output_buffers 32 64k; # 增大输出缓冲,适配大文件
aio on; # 启用异步 IO,避免阻塞 worker
directio 4m; # 超过 4M 的大文件走直接 IO,绕过页缓存
directio_alignment 512;
}
directio 适合超大文件:它绕过操作系统的页缓存,直接从磁盘读入用户空间再发送,避免大文件反复污染页缓存导致其他资源命中率下降。aio 则让磁盘读取不阻塞 worker 进程。这些高级开关只对真实的大文件下载场景有意义,常规 Web 站点不需要也不应该全部打开。
3. 条件请求与 ETag
一句话总结: ETag 与 Last-Modified 让浏览器用 If-None-Match / If-Modified-Since 做验证,资源未变时服务器返回 304 空响应体。
浏览器缓存命中有两种:强缓存(不请求服务器直接使用本地副本)与协商缓存(请求服务器但仅验证「是否变了」)。协商缓存的基础是条件请求。Nginx 默认生成 ETag(基于文件的 mtime 与 size)与 Last-Modified,浏览器下次请求时携带 If-None-Match 或 If-Modified-Since,Nginx 验证未变就返回 304 Not Modified,响应体为空,仅需几百字节开销。
# 条件请求验证:ETag 默认开启,显式声明以便后续微调
server {
listen 80;
server_name example.com;
root /srv/www/static;
location /static/ {
etag on; # 默认开启
# 自定义 ETag 前缀:发布号变化时强制全部失效
etag_format "v1-%x-%x";
add_header Cache-Control "no-cache"; # 每次都要验证,但只传差异
}
}
etag_format 允许自定义 ETag 的生成格式。生产实践中常用「发布版本号 + 文件特征」拼接:当静态资源整体重新部署、版本号变更时,所有 ETag 都变化,浏览器会全量重新拉取,避免因 mtime 巧合导致「内容已变但 ETag 未变」的陈旧缓存问题。
理解 304 与 200 的区别是排查缓存问题的关键:
# 首次请求:200,携带 ETag
curl -sI https://example.com/static/app.css | grep -E 'HTTP|ETag'
# 带上 If-None-Match 再次请求:304,无响应体
curl -s -o /dev/null -w '%{http_code}\n' \
-H 'If-None-Match: "v1-5f3a-2b7c"' \
https://example.com/static/app.css
4. 浏览器缓存策略
一句话总结: Cache-Control 决定资源在浏览器端的缓存生命周期,带指纹的文件用长缓存、可变文件用 no-cache 验证,是静态资源的黄金法则。
条件请求避免了「重新下载」,但浏览器缓存策略决定了「是否还需要发起请求」。Cache-Control 是浏览器缓存行为的权威指令。静态资源缓存有一条黄金法则:带内容指纹(文件名含 hash)的资源可以长期缓存,不带指纹、可能变化的文件应当每次验证。
# 按文件名特征区分缓存策略
server {
root /srv/www/static;
# 带 hash 指纹的资源:一年长缓存,无需回源验证
location ~* \.(css|js)$ {
# 若文件名形如 app.8f3a2b.css 则长缓存;否则短缓存
if ($uri ~ "-[0-9a-f]{8,}\.") {
add_header Cache-Control "public, max-age=31536000, immutable";
}
# 未带指纹的同类文件:短缓存 + 协商
add_header Cache-Control "public, max-age=3600";
}
# 图片与字体:中等时长缓存
location ~* \.(png|jpg|jpeg|webp|svg|woff2)$ {
add_header Cache-Control "public, max-age=2592000"; # 30 天
}
# HTML 页面:绝不长缓存,保证内容即时更新
location ~* \.html$ {
add_header Cache-Control "no-cache"; # 协商验证
}
}
immutable 是给带指纹资源的额外声明:它告诉浏览器「这个资源永远不会变,本地命中了直接使用,连条件请求都不用发」。配合 CDN 时,s-maxage 可以为共享缓存单独设定时长:
add_header Cache-Control "public, max-age=3600, s-maxage=86400";
# 浏览器缓存 1 小时,CDN 缓存 1 天,源站收到未命中请求可 304 快速响应
缓存策略的坑集中在「过期设置太激进导致发布不可见」:CSS/JS 未带指纹却设置了一年缓存,用户就永远看不到新样式。规避办法是发布流程强制指纹化(构建工具自动在文件名里注入内容 hash),并让 HTML 始终 no-cache,这样指纹变了浏览器自然拉新资源。
5. 压缩联动:gzip 与 Brotli
一句话总结: gzip 是普适压缩,Brotli 在相同体积下压缩率更高,Nginx 通过 Brotli 模块或反代层为静态资源叠加更小的传输体积。
传输体积的缩小与缓存互补:缓存管「少传」,压缩管「传得少」。gzip 早已普及,文本类资源(HTML/CSS/JS/JSON/SVG)压缩率通常在 60% 以上。Brotli 是 Google 推出的新一代压缩算法,同等级压缩率比 gzip 高 5%~15%,现代浏览器默认支持,Nginx 通过 ngx_brotli 模块提供支持。
# gzip 基础配置(Nginx 内置)
http {
gzip on;
gzip_comp_level 5;
gzip_min_length 1024; # 小于 1KB 不压缩,避免浪费
gzip_types
text/plain text/css application/json
application/javascript application/xml
image/svg+xml application/font-woff2;
gzip_vary on; # 响应头标记 Vary: Accept-Encoding
}
# Brotli 配置(需要 ngx_brotli 模块)
http {
brotli on;
brotli_comp_level 6;
brotli_min_length 1024;
brotli_types
text/plain text/css application/json
application/javascript application/xml
image/svg+xml application/font-woff2;
}
启用 Brotli 后,Nginx 会按客户端的 Accept-Encoding 协商:支持 Brotli 的浏览器收到 Content-Encoding: br,只支持 gzip 的收到 gzip。gzip_vary on 让响应携带 Vary: Accept-Encoding,避免中间缓存把压缩版本错误地分发给不支持解压的客户端。
# 验证压缩是否生效
curl -sI -H 'Accept-Encoding: br' https://example.com/static/app.js \
| grep -E 'Content-Encoding|Content-Length'
# 期望看到: Content-Encoding: br
压缩的权衡在于 CPU:压缩级别越高,体积越小,但压缩耗时越长。动态响应(HTML)建议用 4~6 级,避免高压缩级拖慢首字节;静态资源因为可以预压缩,可以放开。更彻底的方案是在构建期预生成 .gz / .br 文件,Nginx 用 gzip_static / brotli_static 直接发送预压缩文件,运行时零压缩开销。
# 预压缩文件直发,运行时零压缩开销
http {
gzip_static on; # 有 .gz 文件就发 .gz,否则才现场压缩
gzip_vary on; # 仍然标记 Vary,避免缓存错发
}
# 同理,brotli_static 优先发送 .br 预压缩文件
http {
brotli_static on;
brotli_vary on;
}
# 构建期预生成压缩文件(示例脚本)
gzip -k -9 dist/app.js dist/app.css dist/index.html
brotli -k -q 11 dist/app.js dist/app.css
# 验证:命中了 .br 预压缩文件,Content-Encoding 应为 br
curl -sI -H 'Accept-Encoding: br' https://example.com/static/app.js \
| grep -E 'Content-Encoding|Content-Length'
预压缩需要构建流水线配合:每次构建产出静态文件的同时生成 .gz 与 .br 副本,并把二者一并发布。发布脚本要在部署后校验压缩文件与源文件的时间戳一致,避免「源文件更新了、预压缩文件还是旧的」这类隐蔽问题——此时 Nginx 会发送过期压缩版本,用户拿到旧内容且无法通过 ETag 区分。
6. 缓存命中与 CDN 配合
一句话总结: 静态资源的最优路径是「CDN → 浏览器缓存」两层拦截,源站只服务未命中,配合日志与观测确认命中率。
在浏览器缓存之上还有 CDN 这一层共享缓存。用户请求先命中 CDN 边缘节点,边缘未命中才回源到 Nginx。这样静态资源只在「第一次有人请求」时才打到源站,热资源的源站命中率可以做到接近零。Nginx 作为源站的角色,主要任务是为 CDN 提供正确的缓存指令与稳定的响应。
# 作为 CDN 源站:为共享缓存明确标注可缓存性
location ~* \.(css|js|png|jpg|webp|woff2)$ {
add_header Cache-Control "public, max-age=2592000, s-maxage=604800";
expires 30d;
add_header X-Cache-Status "MISS"; # 观察:未命中时标记
}
# 配合反向代理层,在 Nginx 内部也做一层磁盘缓存
location /static/ {
proxy_cache static_cache;
proxy_cache_key "$uri$is_args$args";
proxy_cache_valid 200 304 7d;
proxy_pass http://origin_static;
}
观测静态资源链路,要在每一层留下命中痕迹。源站侧在访问日志中记录响应码与字节数:200 表示真的回源了,304 表示协商命中,两者都应当在 CDN 命中率报表里趋近于零。CDN 侧则通过命中率指标确认边缘缓存是否正常工作。若 CDN 命中率长期偏低,优先检查源站返回的 Cache-Control 是否允许缓存,以及 Vary 是否导致缓存碎片化。
7. 性能验证与调优
一句话总结: 静态资源性能用「首字节、传输量、命中率」三个指标验证,针对单资源逐个检查响应头即可定位是哪条路径未生效。
验证静态资源服务是否调优到位,可以用三个指标量化。首字节时间(TTFB)反映 sendfile 与网络路径;传输字节数反映压缩是否生效;缓存命中率反映 Cache-Control 与 CDN 配置是否正确。逐个检查响应头就能定位问题:
# 完整查看响应头,核对五件事
curl -sI https://example.com/static/app.js
# 1. Content-Type 是否准确 → 影响浏览器解析
# 2. Content-Encoding 是否 br/gzip → 压缩生效
# 3. ETag / Last-Modified 是否存在 → 协商缓存可用
# 4. Cache-Control 时长是否符合预期 → 浏览器缓存策略
# 5. Vary: Accept-Encoding 是否在 → 避免缓存错发
压测静态资源要区分「纯文件服务」与「小文件高并发」。纯文件服务看吞吐(MB/s)与并发连接数,关注 worker_processes 与 worker_connections 是否匹配;小文件高并发看每秒请求数(rps)与 99 分位延迟,关注 keepalive 是否开启、open_file_cache 是否命中:
# 文件描述符与元数据缓存,显著降低小文件请求的开销
open_file_cache max=10000 inactive=60s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
server {
listen 80;
keepalive_requests 1000; # 单条 keepalive 连接复用更多请求
root /srv/www/static;
}
open_file_cache 缓存文件描述符、大小与 mtime 等元数据,避免每个请求都做一次 stat 系统调用。对于图片站、图标库这类「海量小文件」场景收益明显。压测时还应开启 access_log off 或采用异步日志,排除日志 IO 对结果的影响,得到接近真实能力的数值。
8. 总结
| 环节 | 要点 |
|---|---|
| 性能目标 | 少传、少算、少往返,三条路径叠加 |
| sendfile | 零拷贝直达网卡,tcp_nopush 攒包发送 |
| 条件请求 | ETag/Last-Modified,304 只传差异 |
| 浏览器缓存 | 带指纹长缓存 immutable,HTML 用 no-cache |
| 压缩联动 | gzip 普适 + Brotli 更高压缩率,预压缩零开销 |
| CDN 配合 | 源站标注可缓存性,观察各层命中率 |
| 验证手段 | TTFB、传输量、命中率三个指标逐个核对响应头 |
| 调优细节 | open_file_cache 缓存元数据,keepalive 提升复用 |
静态资源加速的每条路径都简单,但四条路径要协同生效,关键在配置与发布流程的一致性:构建时指纹化资源、Nginx 上分区设置缓存策略、预压缩减少运行时开销、CDN 层承接绝大多数流量。做到这些,静态资源就从「要优化的环节」变成「几乎不占资源的水管」。接下来把视角从性能转到运维,看如何用指标与日志把 Nginx 的运行状态变成可观测的数据。
延伸阅读
- Nginx 缓存与压缩优化 — proxy_cache 与 gzip/Brotli 深入
- Nginx 核心配置结构 — root 与 alias、location 匹配基础
- Nginx HTTPS 与 TLS 加固 — 静态资源走 HTTPS 的证书配置
- Nginx 反向代理与负载均衡 — 反代层缓存与转发链路
- Nginx 日志分析与性能调优 — 用日志验证缓存与压缩效果
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。