
记得我第一次系统性地梳理前端网络请求这块内容是在带一个中大型项目遇到性能瓶颈的时候。页面首屏渲染其实没多少复杂逻辑但用户反馈就是慢打开控制台一看光接口请求就串行了十几秒。那时候我才意识到很多写了好几年业务代码的开发者发请求靠的是框架封装好的方法改参数靠查文档至于请求到底怎么从浏览器走到服务器、为什么有的接口快有的接口慢、哪些请求能合并哪些不能脑子里其实是一笔糊涂账。这篇文章我就围绕网络请求这个具体场景把进阶技巧和底层原理这两块掰开揉碎讲清楚。适合两类人看一类是写过一段时间业务、想往更高处走一走的前端开发者另一类是后端同学想理解前端的网络优化手段到底在优化什么。我不会堆概念每个技巧都会落到具体场景和可操作的配置上同时把背后的机制讲透——只有理解了原理技巧才不会变成死记硬背的招数。1. 一次完整的网络请求从URL输入到响应返回很多进阶技巧之所以让人感觉学了用不上根源在于对请求的生命周期没有整体认知。你可以把一次网络请求想象成一次跨城市快递从你敲下回车到页面展示数据中间经历了寄件、运输、分拣、派送多个环节每个环节都有独立的规则和开销。1.1 DNS解析地址簿查询决定了第一跳输入URL之后浏览器第一件做的事不是连接服务器而是查地址簿——也就是DNS解析把api.example.com这样的域名翻译成服务器真正的IP地址。这个环节常常被忽略但它的耗时波动非常大。本地DNS缓存命中时可能只要几毫秒缓存过期则需要完整走一遍递归查询耗时可以达到几十甚至上百毫秒。我建议你在项目里实际测一下这个数据。用curl -w命令可以直观看到每个阶段的耗时分布curl -o /dev/null -s -w DNS解析: %{time_namelookup}s\nTCP连接: %{time_connect}s\nTLS握手: %{time_appconnect}s\n首字节时间: %{time_starttransfer}s\n总耗时: %{time_total}s\n https://api.example.com/v1/users我自己做性能排查时第一件事就是看这组数据。如果DNS解析和TCP连接两项加起来超过总耗时的30%说明问题大概率出在链路上而不是服务端逻辑优化方向完全不同。1.2 TCP握手与TLS协商每个新连接都有固定成本TCP握手是三次挥手——SYN、SYN-ACK、ACK这个过程的往返时间也就是常说的RTTRound-Trip Time。如果客户端和服务器物理距离远一次RTT可能就要50毫秒以上三次握手至少150毫秒。再加上HTTPS的TLS协商通常需要额外1到2个RTT一个新连接建立下来300到500毫秒的固定开销就出去了。这也是为什么HTTP/1.1时代浏览器会限制同域并发连接数Chrome是6个不是浏览器抠门而是每个连接都有实打实的建造成本。明白这一点你就能理解后文讲的连接复用和域名分片到底在解决什么问题。1.3 HTTP请求发送与响应返回排队和传输的博弈连接建立之后请求本身通过TCP协议栈拆分、发送、重组。这里有两个关键机制一是TCP的慢启动。新连接刚建立时发送窗口很小拥塞窗口从初始值大约10个MSS即最大报文段长度开始逐步翻倍直到到达阈值或出现丢包。一个需要传输大文件的请求前几个RTT的吞吐量远低于链路带宽连接复用能有效规避这个起步慢的问题。二是HTTP层面的队头阻塞。HTTP/1.1的Keep-Alive复用了TCP连接但同一个连接上的请求必须按顺序处理——前一个请求没返回后一个请求就得排队等。这就是为什么页面同时加载几十个资源时Performance面板里能看到一串长长的排队中状态。2. 连接复用与多路复用把固定成本摊薄理解了每个连接都有固定成本之后优化思路自然就清晰了能复用连接就复用连接能一股脑并发就不要串行等待。2.1 Keep-Alive的配置门道HTTP/1.1默认开启Keep-Alive让TCP连接在请求完成后保持一段时间后续请求复用同一连接。Node.js服务端通过server.keepAliveTimeout来控制连接空闲多久后关闭默认是5秒。之前排查过一个诡异问题接口偶尔多出近一秒延迟抓包发现是Nginx和Node服务端的keepAliveTimeout配置不一致——请求到了旧连接上Nginx还在等Node已经关了客户端只能重新建连白白浪费一次完整的握手开销。const server http.createServer(app); // 单位是毫秒建议不要小于Nginx的keepalive_timeout server.keepAliveTimeout 65000; server.headersTimeout 66000;这个配置本身不难但要注意和反向代理的配合。Nginx默认keepalive_timeout是75秒如果Node这边设成5秒就意味着用户的请求到达Nginx后Nginx到Node的上游连接大概率已经断了每次都要重建。调成比上游代理稍大的值可以让链路尽量维持在同一批连接上。2.2 HTTP/2多路复用真正解决队头阻塞如果说Keep-Alive是省连接钱HTTP/2的多路复用就是直接把队头阻塞消灭在协议层。HTTP/2在单一TCP连接上引入流Stream和帧Frame的概念多个请求可以同时交错传输不需要排队。启用HTTP/2的收益非常直观。我实测过一个包含80多个静态资源的页面HTTP/1.1下资源加载瀑布图上有明显的串行和排队段切到HTTP/2之后整个瀑布图变得非常密集总加载时间大约减少了35%到45%。这个优化不需要改一行业务代码只需要在Nginx开启listen 443 ssl http2;前提是证书和现代浏览器支持。国内大会员区的网络环境里部分老旧系统可能不兼容这个问题我们在部署时单独做了降级处理不影响HTTP/1.1用户。2.3 连接复用遇到负载均衡时的注意事项连接复用不是万能的一旦中间加上了负载均衡情况会变得复杂。以Nginx为例默认负载均衡算法是轮询但如果配置了keepalive指令而没有正确设置长连接池或者后端节点扩容后连接分布不均就会出现某些节点连接数畸高、某些节点空闲的情况。遇到这类问题不要急着调算法先看连接分布。我曾经在压测时发现某台后端机器的TCP连接数是其他机器的三倍最后定位到原因是Nginx配置了基于IP的hash算法而压测流量恰好来自同一个出口IP流量全部打到了同一台机器上。这里没有标准答案方案取决于业务场景需要会话保持就用ip_hash不需要就交给默认的加权轮询关键是要知道每个配置会导致什么后果。3. 缓存策略进阶让浏览器替你干活缓存是最容易被低估的优化手段。一个配置得当的缓存策略能让大部分静态资源请求直接命中本地缓存连网络都不走。3.1 强缓存与协商缓存的配合强缓存就是Cache-Control: max-agexxx在有效期内浏览器直接使用本地副本完全不发请求。协商缓存则是带上If-Modified-Since或If-None-Match去问服务器这个资源变没变服务器返回304就继续用旧的。我见过不少团队要么完全不配置缓存要么一刀切max-age31536000。前者的后果是资源重复加载后者的后果是发布新版本后用户死活看不到更新。正确的做法是区分资源类型带内容hash的文件名如app.8f3d2a.js可以放心大胆地设置max-age31536000因为内容变了文件名就会变不存在缓存不更新的问题。不带hash的入口HTML如index.html则应该设置no-cache确保每次都回源验证一下拿到最新的资源引用。location /static/ { expires 1y; add_header Cache-Control public, immutable; } location /index.html { add_header Cache-Control no-cache; }这套组合拳打下来用户第二次访问的资源加载基本走本地缓存页面速度感知提升非常明显。3.2 按需拉取的缓存更新强缓存的时间窗口内服务端如果更新了资源客户端拿不到最新版本。对于非静态资源类接口比如用户信息、商品列表不要用强缓存应该走协商缓存。另有一些数据比如配置信息、字典表可以在业务层面做定时刷新版本号的策略。举个实际例子我们做活动页时运营会频繁修改活动配置但配置本身在客户端被CDN强行缓存了15分钟。后来我把配置接口改成ETag Cache-Control: no-cache客户端每次请求都会带条件头到源站验证配置没变就返回304几乎零成本配置变了立即生效。这个改动完全不增加服务端压力却把配置生效延迟从最长15分钟压缩到秒级。3.3 CDN缓存下的灰度发布问题有CDN的情况下缓存策略要留一手。CDN节点会按照你的Cache-Control头来缓存内容但不同CDN厂商对缓存刷新的语义理解不完全一样。有的刷新是全节点立即回源有的是逐节点慢慢过期。做灰度发布时我一般建议在URL上带版本参数来实现CDN层面的隔离https://cdn.example.com/app.js?v20240615。虽然从严格意义上说这不是最优雅的方案但它是能被所有CDN正确理解的方案几乎没有歧义。等你对CDN厂商的缓存刷新机制摸得很透了再考虑去掉版本参数。4. 实战中的性能排查一步步定位瓶颈在哪说完了优化手段接下来聊聊怎么定位瓶颈。很多人在DevTools的Network面板面前只会看哪个请求慢然后找后端要说法。其实从浏览器发出到响应返回的每一段耗时都能拆出来看。4.1 用Performance面板看资源加载瀑布图Performance面板的瀑布图Waterfall是排在前端侧的第一个工具。每一行资源的耗时被拆分成多个阶段Queueing排队、Stalled停滞、DNS LookupDNS查找、Initial Connection建连、SSLTLS握手、Request/Response请求/响应。我的排查习惯是先按颜色看阶段分布。如果大量资源都卡在Queueing和Stalled说明是并发请求数超过了浏览器限制或者有长请求占着连接不放优先考虑资源合并或HTTP/2。如果DNS Lookup普遍偏大优先考虑DNS预解析。如果Initial Connection普遍偏大优先考虑连接复用和CDN。这个排序能帮你快速圈定问题类型而不是盲目优化。4.2 从服务端日志反推耗时都花在哪了浏览器侧看到的总耗时包含网络传输时间为了区分网络慢和服务端慢需要在服务端加响应毫秒数的日志。比如Express里记录每个请求的耗时app.use((req, res, next) { const start Date.now(); res.on(finish, () { const cost Date.now() - start; console.log(${req.method} ${req.url} ${res.statusCode} ${cost}ms); }); next(); });通过对比同一接口在浏览器瀑布图里的总耗时与服务端日志里的处理耗时就能估算出网络传输占了多大比例。如果处理耗时只有20毫秒浏览器侧却耗时800毫秒那问题几乎肯定出在网络上反过来如果服务端本身就处理了700毫秒那再怎么优化前端也没用。4.3 用抓包工具看真实链路浏览器DevTools能看到的部分以浏览器为边界再往下的TCP重传、拥塞控制行为就看不到了。排查难以复现的偶发性能问题时抓包是最后的底牌。用tcpdump抓取特定端口流量sudo tcpdump -i any -nn host 目标服务器IP and port 443 -w network.cap抓到的pcap文件用Wireshark打开重点看TCP的Seq/Ack序列和重传标记。有一次线上偶发请求超时浏览器侧一直显示Stalled状态抓包发现TCP层出现了大量重传原因是机房链路的MTU配置过大导致分片被中间设备丢弃。这类问题不抓包根本找不到根因你在代码层面上做任何优化都是徒劳。5. 容易踩的坑这些配置坑我帮你趟过了5.1 跨域请求的预检开销非简单请求比如Content-Type: application/json的POST、带自定义header的请求会先发一个OPTIONS预检请求服务端返回允许后浏览器才发送真正的请求。每个新请求多一次额外的往返在高频请求场景下成本不小。优化手段是让接口支持简单请求或者服务端在响应里加Access-Control-Max-Age让浏览器缓存预检结果add_header Access-Control-Max-Age 86400;另外一点开发环境配了跨域代理后要确保联调时真正请求的是代理地址而不是直连后端否则一堆预检请求全打到后端不说还可能因为后端没配CORS导致请求直接失败。这个坑很基础但我在团队里确实见过有人排查了半天接口为什么没有响应头。5.2 请求并发限制没算上预检和图片浏览器的同域并发限制针对的是同域连接而不是同一页面的请求总数。假设你的页面跨域请求了三个不同域名的接口每个域都能开6个连接看起来并发上限是18个但每个跨域非简单请求都要先消耗一个预检请求的往返时间。我曾经在一个活动页上看到十几张图片和七八个接口同时发起图片是CDN域名接口是另一个API域名请求总数远超想象加上部分接口触发了预检整个页面在低端手机上加载得像爬一样。后来做了三件事小图全部转成WebP并合并成雪碧图接口域名收敛到同一个并开启HTTP/2预检结果缓存加长。页面加载时间从约7秒降到了2秒出头措施其实不复杂关键是先看清请求拓扑。5.3 缓存更新不生效的经典场景设置了max-age31536000并且文件名带hash后最常见的缓存不更新其实是入口HTML被缓存了。比如index.html被CDN或浏览器缓存住了页面引用的还是旧hash的资源文件名。排查方法是打开DevTools勾选Disable cache后强制刷新如果问题消失说明缓存链路的入口策略有误——入口文件一定不能强缓存。还有一种隐蔽情况某些Web服务器默认对没有设置缓存头的请求返回Last-Modified走协商缓存而非强缓存。你以为没有强缓存就每次回源实际可能被路由缓存或代理缓存帮忙缓存了。这时要看响应头里的X-Cache: HIT之类的字段判断是哪个环节缓存住了然后针对性设置Cache-Control: private或no-store。5.4 压测数据很好看线上却慢调试网络性能时最误导人的环节就是压测。压测工具和浏览器在连接复用、TLS会话恢复、请求并发模型上都不相同压测结果只能当作基准参考不能当作线上预期。一个典型场景ab压测默认关闭Keep-Alive每个请求重新建连结果得出的QPS远低于开启Keep-Alive的真实表现而反过来用wrk压测时默认并发连接数很高得出的延迟数据又比真实用户低很多。更贴近线上的是用真实浏览器录制用户行为回放或者使用puppeteer模拟一次完整的页面访问再结合Performance API输出指标const perfData performance.getEntriesByType(navigation)[0]; console.log(DOMContentLoaded:, perfData.domContentLoadedEventEnd); console.log(Load:, perfData.loadEventEnd);结论是压测只用来横向对比自己优化前后的变化去判断优化方向是否正确不要拿它去预测线上体验。6. 一套完整的网络优化检查清单最终把文章里提到的各种方法汇总成一套可执行的检查清单。每次接手新项目或者线上反馈页面变慢了我都是按这个顺序过一遍基本能定位八成以上的问题。6.1 基础设施层确认服务端和Nginx的Keep-Alive配置匹配避免连接被半途关闭确认HTTP/2已开启且证书无兼容性问题确认CDN覆盖和回源链路检查X-Cache命中率确认DNS解析时间大于50ms就考虑换DNS服务商或加DNS预解析6.2 资源层静态资源带内容hash设置max-age31536000入口HTML设置no-cache合理使用WebP/AVIF图片格式小图尽量合并减少同页面的跨域请求收敛域名6.3 接口层对不适合强缓存的接口配置ETag协商缓存预检请求设置Access-Control-Max-Age服务端日志记录每个请求的耗时方便区分网络慢还是服务端慢大接口按需分页或裁剪字段减小传输体积我把这张清单打印出来贴在工位上每次排查问题时逐项核对比漫无目的地翻DevTools高效得多。你在自己的项目里可以先跑一遍八成能发现至少一两个从来没注意过的隐藏开销。这篇文章从请求生命周期讲到缓存策略从性能排查讲到避坑心得核心就一句话网络优化的本质是对固定成本的管理——建连是成本、排队是成本、缓存不命中是成本。理解了这些成本在哪里产生技巧自然就有了用武之地。如果你的项目也存在代码看起来没问题但就是加载慢的困扰不妨按这个思路排查一轮大概率会有惊喜。