最近给系统做前端性能排查,盯着 Chrome DevTools 的 Network 面板直接无语了。所有的 API 请求在真正发送之前,浏览器都要莫名其妙先发一个 OPTIONS 请求。
关键是这笔过桥费还贼贵!我们的微服务网关部署在海外,普通的 GET 或 POST 接口响应时间大约在 150ms。但因为这个多出来的 OPTIONS 预检请求(Preflight Request),用户发个言、点个赞需要等上整整 350ms 以上。网络不好的时候,双倍的往返时间(RTT)能让前端卡得像 PPT。
开发环境本地跑的时候因为延迟低没感觉,一到生产环境,这种白痴问题就成了摧毁用户体验的隐形杀手。
为什么会平白无故多出 OPTIONS 请求?
简单来说,这是浏览器的 CORS(跨域资源共享)安全机制在作祟。当满足以下任一条件时,浏览器会认定该请求为「非简单请求」,在发送真实数据前,必须发送一次 OPTIONS 请求询问服务器是否允许此次跨域:
- 请求方法不是
GET、POST或HEAD。 Content-Type设为了application/json(现代 SPA 几乎全是这个)。- 请求中包含了自定义的请求头(比如我们带在 Header 里的
Authorizationtoken)。
既然我们的业务请求百分之九十都逃不过这些条件,那是不是每次发请求都要白白浪费一次 RTT?
当然不是。浏览器其实提供了一个缓存机制,允许我们把这个预检结果缓存起来,在一段时间内不用重复询问。
Access-Control-Max-Age 登场
要让浏览器记住跨域许可,后端需要在 OPTIONS 的响应头中加入:
Access-Control-Max-Age: 86400
这个值代表缓存的秒数。只要在有效期内,浏览器再次向该域名发送相同路径和方法的跨域请求时,就会直接略过 OPTIONS,秒发真实的业务请求。
然而,不同浏览器对这个最大缓存时间的限制是不一样的:
- Firefox 的上限是 24 小时(86400 秒)。
- Chrome / Chromium 系列 的上限只有 2 小时(7200 秒)。
如果你设置了 86400,Chrome 会自动降级成 7200。虽然只有 2 小时,但对于日常用户高频操作的页面来说,已经能干掉 90% 以上无意义的 OPTIONS 请求了。
生产级 Nginx 网关配置
在业务服务里去写跨域 Header 非常痛苦,而且增加了代码的心智负担。最科学的方案是在 Nginx 网关层一把梭搞定。
我们需要做到两点:
- 直接在 Nginx 拦截 OPTIONS 请求,不往后端的业务服务(Node.js/Go/Java)转发,直接返回
204 No Content,节省后端算力。 - 正确返回 CORS 相关的 Header,特别是
Access-Control-Max-Age。
以下是亲测有效的生产环境 Nginx 配置:
server {
listen 80;
server_name api.example.com;
location /api/ {
# 定义需要支持的跨域源,生产环境尽量不要写通配符 *
set $cors_origin "";
if ($http_origin ~* (https?://(?:.+\.)?yourdomain\.com)$) {
set $cors_origin $http_origin;
}
# 如果是预检请求 OPTIONS,直接在 Nginx 层处理并返回
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;
# 核心:配置预检请求的缓存时间(设置为 2 小时,贴合 Chrome 的上限)
add_header 'Access-Control-Max-Age' 7200 always;
add_header 'Content-Type' 'text/plain; charset=utf-8' always;
add_header 'Content-Length' 0 always;
return 204;
}
# 针对真实的 GET/POST 等请求,也要带上跨域头
if ($request_method ~* (GET|POST|PUT|DELETE)) {
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
}
# 反向代理到后端业务集群
proxy_pass http://backend_upstream;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
踩坑复盘:跨域安全与凭证
配置完后,一定要确保前端在发送带 Cookie/Credentials 的请求时,Access-Control-Allow-Origin 不能是 *,必须是具体的域名(这就是为什么我在 Nginx 里写了一段 $cors_origin 正则匹配)。
上线这个配置后,不仅接口响应平均时间下降了近一半,服务器的吞吐量也有了小幅上升,毕竟 OPTIONS 请求也是需要消耗系统连接数和 CPU 的。
你们在做跨域设计时,是倾向于在 Nginx 网关层一把梭配置,还是老老实实通过 BFF 做同源转发?