静态资源服务与缓存策略
1. 概念与原理
前端构建产物(JS、CSS、图片、字体、HTML)的终极归宿:CDN 边缘节点或 Nginx 静态服务。Nginx 直接服务文件的性能远优于 Node、Python 任何应用服务器,核心在于 sendfile 零拷贝和内核 splice。
2. 基础静态配置
server {
listen 80;
server_name static.example.com;
root /var/www/dist;
index index.html;
# 安全:禁止列目录
autoindex off;
# 隐藏文件不服务
location ~ /\. {
deny all;
return 404;
}
# MIME 类型
include mime.types;
default_type application/octet-stream;
types {
application/wasm wasm;
font/woff2 woff2;
}
}
2.1 root vs alias
# root:location 路径 拼到 root 后
location /static/ {
root /var/www; # 实际找 /var/www/static/xxx
}
# alias:location 路径 替换为 alias
location /static/ {
alias /var/www/dist/; # 实际找 /var/www/dist/xxx
}
最常见错误:alias 结尾漏斜杠 → 路径拼接出错。
3. sendfile 与零拷贝
sendfile on; # 内核直接把文件从磁盘送到 socket,不经过用户态
tcp_nopush on; # 配合 sendfile,整个响应拼好再发(减少小包)
tcp_nodelay on; # keep-alive 连接上关 Nagle,响应快
性能差异:关闭 sendfile(传统 read+write)vs 开启,吞吐量差 2-5 倍。
4. 压缩
4.1 gzip
gzip on;
gzip_vary on; # 响应带 Vary: Accept-Encoding
gzip_proxied any; # 代理请求也压
gzip_comp_level 5; # 1-9,5 是性能/效果平衡点
gzip_min_length 256; # 小于 256B 不压(header 开销反而大)
gzip_types
text/plain
text/css
text/javascript
application/javascript
application/json
application/xml
image/svg+xml
font/woff2;
# 注意:不要对已压缩格式(jpg、png、woff2、mp4)gzip
4.2 Brotli(推荐)
比 gzip 再小 15-25%。需要模块 ngx_brotli:
brotli on;
brotli_comp_level 6; # 1-11,6 动态压平衡点
brotli_types text/plain text/css application/javascript application/json image/svg+xml;
brotli_static on; # 如果有 .br 预压缩文件直接用
构建时预压:
# webpack / vite 插件输出 .br 和 .gz 文件
# Nginx 先找 .br,没有再动态压
4.3 静态预压与运行时压缩
gzip_static on; # 有 .gz 预压缩文件就直接返回,不用 CPU 实时压
brotli_static on;
构建流水线产出 .br / .gz 文件,Nginx 直接返回,零 CPU 开销。
5. 缓存策略
5.1 静态资源(hash 文件名)
location ~* \.(js|css|woff2?|ttf|svg|png|jpg|jpeg|webp|avif|gif|ico|mp4|wasm)$ {
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable";
access_log off; # 减少 IO
tcp_nodelay off; # 大文件关 nodelay
}
immutable 告诉浏览器"这个资源绝对不变",不需要协商。前提:文件名含 hash(app.abc123.js),内容变 hash 变 URL 变。
5.2 HTML(入口文件)
location = / {
add_header Cache-Control "no-cache";
try_files /index.html =404;
}
location / {
add_header Cache-Control "no-cache";
try_files $uri $uri/ /index.html;
}
no-cache = 每次验证。浏览器发 If-None-Match/If-Modified-Since,命中返回 304。
5.3 ETag 与 Last-Modified
Nginx 默认开启:
etag on; # 自动计算 ETag(基于文件 mtime + size)
if_modified_since before; # exact = 精确匹配,before = 早于就 304
手动关(CDN 后不需要):etag off;
5.4 防缓存穿透
某些请求应该 no-store:
location = /api/user {
add_header Cache-Control "no-store, private";
proxy_pass http://backend;
}
6. 安全头
# 统一在 server 块里加
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
always = 不管状态码都加(包括 4xx 5xx)。
7. CORS
location /api/ {
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin $http_origin;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-Requested-With";
add_header Access-Control-Max-Age 86400;
add_header Content-Length 0;
return 204;
}
add_header Access-Control-Allow-Origin $http_origin always;
add_header Access-Control-Allow-Credentials true always;
add_header Vary Origin always;
proxy_pass http://backend;
}
Vary: Origin 必须加,否则 CDN 缓存第一个 Origin 返回给后续所有不同 Origin。
8. 大文件与 Range
location /video/ {
root /var/www/media;
mp4; # MP4 流式支持
mp4_buffer_size 1m;
mp4_max_buffer_size 5m;
# 限速(防单用户吃满带宽)
limit_rate_after 10m; # 前 10MB 不限
limit_rate 1m; # 之后限 1MB/s
}
Nginx 自动处理 Range 请求(206 Partial Content),无需额外配置。
9. 故障排查
9.1 静态资源 403
namei -l /var/www/dist/index.html
# 看每一级目录 nginx 用户有没有 x 权限
# 检查 SELinux
getenforce
ls -Z /var/www/dist/
# 如果有 SELinux 标签问题
chcon -R -t httpd_sys_content_t /var/www/dist
9.2 MIME 类型错误
浏览器 console Resource interpreted as Stylesheet but transferred with MIME type text/plain。
include mime.types; # 确保在 http 块里 include 了
或手动加类型:types { application/javascript mjs; }
9.3 缓存策略没生效
curl -I https://example.com/app.js | grep -i cache
# 看 Cache-Control 是否正确
# 注意 add_header 在同一 location 层级会覆盖上层所有 add_header
10. 常见反模式
expires -1当不缓存:实际是过期 = 过去时间 = 协商缓存。要用no-storeautoindex on:暴露目录结构,安全隐患- 不加
gzip_vary:CDN 不知道有 gzip/非 gzip 两个版本,缓存冲突 - 对 jpeg/png gzip:已压缩格式再压无意义 + 浪费 CPU
gzip_comp_level 9:CPU 爆,收益递减(5→9 只小 3-5%)- hash 文件用
no-cache:浪费协商请求。应该immutable+ 1 年 - HTML 用
max-age=3600但不带 hash:用户 1 小时内看不到新版本 add_header在 location 和 server 混用:location 里加了任何add_header,server 级的全部被覆盖(除非用always)- 不限大文件速率:一个用户吃光带宽
11. 延伸阅读
- Nginx Static Content
- HTTP Caching RFC 9111
- web.dev: HTTP Cache — Google 缓存策略指南
- ngx_brotli 模块