一、先分清"加载慢"和"渲染慢"
很多人问"我的网站打开要三秒,怎么办",一问细节就露馅:三秒是从点击到完全加载(load 事件),还是从点击到看到东西?这两个数字背后是完全不同的两套系统。
把一次页面打开拆成四段时间,每段有自己的负责人:
第一段是网络与服务器:DNS 解析、TCP/TLS 握手、请求发出去到字节回到本地。这一段慢,说明服务器算得慢或线路绕。第二段是关键资源下载:HTML 拿到之后,浏览器还要下载 CSS、字体、首屏图片。这一段慢,说明资源太多太大或串行下载。第三段是渲染:浏览器解析 HTML、构建 DOM 与 CSSOM、布局、绘制。这一段慢,通常是被 CSS 阻塞、字体跳变或过多图层拖住。第四段是剩余资源:折叠以下的图片、异步脚本、统计代码。这一段慢不影响"看到",只影响 load 事件。
中间件层(广告脚本、第三方统计、客服组件)往往落在第四段,但它们经常被当成"首屏慢"的元凶,其实它们不背这个锅。真正的教训是:把症状和段落对应上,再去找那一段的负责人。
二、在服务器侧先切一刀:TTFB 的三个组成
TTFB(Time To First Byte,首字节时间)是唯一横跨客户端和服务器的指标。它的组成是三段:TCP 握手 + TLS 握手 + 服务器处理时间。在 Nginx 里可以通过 $request_time 看到服务端从收到请求到发完响应的全部耗时:
log_format timing '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent $request_time '
'$upstream_response_time $upstream_connect_time';
access_log /var/log/nginx/access.log timing;改完 reload 之后,找 $request_time 最大的那批请求:
awk '{print $NF, $0}' /var/log/nginx/access.log | sort -rn | head -20如果 $request_time 是 0.003 秒,而 $upstream_response_time 是 2.1 秒,说明网关本身很快,慢在背后的 PHP-FPM 或 PHP 应用。反过来,如果 $request_time 很大而 upstream 时间是 -(表示没有走 upstream),说明慢在 Nginx 自己——静态文件读慢、带宽打满,或者 limit_req 把请求排队了。
这里最常见的误判是:以为服务器慢,其实是带宽被打满。带宽满了的表现是并发请求一齐变慢,而不是某一个接口慢。判断方法是看同一时刻不同 URL 的 $request_time 是否同时变大,如果是,去查流量,别查代码。
三、分清"多次请求"和"单次请求慢"
一个页面如果引了 40 个资源,每个资源 100 毫秒,理论上理想情况是并行,实际浏览器对同一域名有并发上限(HTTP/1.1 是 6 条连接)。也就是说串行化是天然存在的。所以第一步永远是减少请求数,而不是先想合并压缩。
用 curl 的 -w 参数可以拿到分段时间,一行看清问题出在哪个阶段:
curl -s -o /dev/null -w 'dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total}\n' 'https://example.com/'读这三个差值:time_connect - time_namelookup 是 TCP 握手耗时(正常 10~80 毫秒,跨洲会到 200 毫秒以上);time_appconnect - time_connect 是 TLS 握手耗时(正常 20~120 毫秒);time_starttransfer - time_appconnect 才是服务器真正算的时间。
如果 DNS 就花了 300 毫秒,那后面所有优化都白做。这类问题的修法是把域名的 NS 换到就近的解析服务,或者干脆开启本地 hosts 缓存做对比实验。如果 TLS 花了 800 毫秒,检查是不是服务器在每次连接时都在做动态证书查询或者 OCSP 校验回源超时。
四、用 --remote-dns 和 --remote-ip 排除"本地 DNS 干扰"
很多站长在自己电脑上测很快,用户反馈慢。八成是本地 DNS 缓存了正确 IP,而用户的递归 DNS 拿到了另一个(更远的)节点。要模拟陌生用户的体验,可以在 curl 里指定远端 IP 绕过本地解析:
curl --resolve example.com:443:104.21.x.x -s -o /dev/null \
-w 'connect=%{time_connect} ttfb=%{time_starttransfer} total=%{time_total}\n' \
https://example.com/这一步最大的价值不是测速,而是把 DNS 的因素从测量中排除掉。如果你用 --resolve 之后速度正常,而正常访问速度慢,那就锁定问题在 DNS 或者线路选择上,不用再看服务器。
五、前端侧的四把尺子:LCP、CLS、INP、TTFB
Core Web Vitals 已经被搜索引擎用作页面体验的信号,它们恰好覆盖了上面说的四个段落。这里不讨论怎么"刷分",只讨论怎么读数据。
LCP(Largest Contentful Paint)衡量的最大内容块什么时候出现,最大内容块通常是首屏大图或大标题。如果 LCP 是 4 秒,但首屏图片只有 200 KB,那问题多半不是图片,而是它被 CSS 或字体阻塞了加载时机。常见错误是给首屏大图加 loading="lazy",那等于故意延迟它的加载。
CLS(Cumulative Layout Shift)衡量布局跳动。CLS 大的站点,常见原因是图片没有宽高属性,字体加载后重新排版,广告位没有预留高度。修法很简单:给所有图片加 width/height,给广告位一个 min-height。
INP(Interaction to Next Paint)衡量交互的响应,反映了 JavaScript 主线程是否被长时间占用。INP 差通常意味着有沉重的第三方脚本或一次性渲染了太多 DOM 节点。这一项很难靠配置修,属于代码问题。
六、第三方脚本的"隐蔽成本"
一个站长的页面打开了四个第三方:统计一个、广告一个、字体一个、客服一个。每个都声称"轻量",但实际各自加载 100~300 KB 的 JS,外加 3~5 个请求。其中广告脚本最容易出问题:它在主线程做同步阻塞,还可能拉来一串二级请求。
判断第三方脚本是否在拖慢页面,看的是"是否存在网络空闲期"。用 Chrome DevTools 的 Performance 面板录制一次加载,看主线程的长任务(红色三角),如果长任务集中在某个第三方域上,那它就是在阻塞首屏。
常见的处理方式有三个,按代价从低到高:一是把非关键脚本改成 defer 或 async;二是把不是首屏必需的脚本放到用户交互之后(比如展开评论区再加载评论系统);三是把脚本移到独立子域或影子 iframe 中,避免共享主线程。第一个最容易,第三个最彻底。
七、一个可复用的诊断流程
总结成一套顺序,遇到"网站慢"就照这个顺序走,不要跳步:
第一步,curl 分段测三次,看 ttfb 是否稳定。稳定看第二步,不稳定看第八步。第二步,看 Nginx 的 $request_time 与 $upstream_response_time,定位慢在网关还是应用。第三步,如果慢在应用,打开 PHP 的慢日志(slowlog),看是哪个脚本、哪个函数。第四步,如果慢在网关,看静态文件命中率与带宽,排查 sendfile 与磁盘 IO。第五步,如果服务器侧都正常,回到浏览器 Performance 面板,看是不是被第三方脚本或 CSS 阻塞。第六步,如果都正常,用 --resolve 测不同地区节点,查线路问题。第七步,确认发版前后有没有变化,比如新加了一个统计、换了一个主题。第八步,如果 curl 结果每次差异巨大,说明是网络抖动或者 conntrack/带宽问题,不是代码问题。
八、三个常见的过度优化
第一个是无脑开 gzip 而又开启 brotli,两个同时压在 Nginx 上并不翻倍,反而在协商阶段有额外开销,正确做法是看客户端 Accept-Encoding 选一个。第二个是全站上 Cache-Control: max-age=31536000,对静态资源是对的,但对 HTML 是灾难,用户永远看不到新内容。第三个是把所有图片都懒加载,首屏图片懒加载是自伤,因为它的加载被推迟到 JS 执行之后。
还有一个更隐蔽的过度优化:为了追求 lab 数据里的高分,把首屏以外的所有内容都改成异步渲染。结果是搜索引擎抓到的 HTML 里只剩一个空壳,用户搜索到你,点进来还要再等一次 JS 执行。这种改法在测速工具上分数漂亮,在真实收录与转化上是负收益。凡是让 HTML 变空的优化,都要重新想一遍。
另外一个常见误区是把 CDN 当成万能加速器。CDN 只能加速静态资源的传输距离,对动态请求的 TTFB 改善有限,因为回源那一段照样要跑。如果你测出的是 HTML 的 TTFB 很慢,上 CDN 之后只有第一个用户变快,后面命中缓存的用户才会明显变快,而你如果对 HTML 设了短 TTL,那这一点优势也就没了。
还有一种情况值得单独说:服务器上跑着两套 PHP,一套是系统自带的 7.4,一套是宝塔或者编译安装的 8.1,而 Nginx 连的 FPM 是哪个并不明确。ps aux | grep php-fpm 能看到监听 socket 的路径,fastcgi_pass 指向的 socket 与它是否一致,才是真正生效的那一套。换过 PHP 版本之后变慢,九成是落到了旧版本的 FPM 上,而 opcache 没有预热,所有请求都在重新编译脚本。
关于磁盘 IO 这一块也值得给一条判据:iostat -x 1 5 看 %util,如果长期在 90% 以上,说明磁盘已经打满,静态文件再怎么调 sendfile 也没用,得换 SSD 或者把静态资源挪到对象存储。很多"网站慢"到最后的结论其实就是这一条。
最后提醒一点:所有优化都应该一次只改一项,改完测一次,记下数字。同时改五个地方,快了你不知道是哪个起作用,慢了你也不知道该回退哪个。这个纪律比任何具体技巧都值钱。
九、小结:先切段,再切责
网站慢不是玄学,而是四段链路里的一段或几段出了问题。所有有效的排查都遵循同一个原则:把"慢"切成可以测量的小段,然后给每一段指定负责人。没有测量就开始优化,多半是把性能从 3 秒优化到 3.2 秒,还多了一个加班的夜晚。
如果你只记一句话,就记这个:先 curl -w 分段,再看 $request_time 与 upstream 时间,最后才看前端。这个顺序能省下大部分误改。