2017 年做练习页面时,我引入了一张接近 5 MB 的背景图,又从两个公共 CDN 加载脚本。开发服务器在本机看起来还行,换到手机热点后首屏要四秒多。我第一反应是“JavaScript 太慢”,于是删了几个循环,几乎没有变化。
老师让我打开 DevTools 的 Network 面板,看 Waterfall。那是我第一次意识到,请求的“耗时”不是一个整体。浏览器可能在等连接槽位,做 DNS 查询,建立 TCP/TLS,等待服务器首字节,最后才下载响应。
图 1:优化之前先找到时间花在哪个阶段;不同阶段需要完全不同的处理。
Waterfall 里的每一段都在说话
我把当时最慢的几类资源整理成表:
| 现象 | Waterfall 特征 | 当时的根因 |
|---|---|---|
| 背景图很慢 | Download 很长 | 图片体积过大,没有压缩 |
| 第三方脚本启动慢 | DNS + SSL 很长 | 来自新的跨域主机,无法复用连接 |
| API 白等一秒 | TTFB 很长 | 服务端故意 sleep 的模拟接口 |
| 多张小图排队 | Queueing/Stalled 很长 | 同源并发连接受限 |
| 刷新后突然变快 | Size 显示 memory/disk cache | 命中浏览器缓存 |
“接口慢”只有在 TTFB 主要由服务端等待组成时才成立。如果 DNS 就花了八百毫秒,改数据库查询没有任何帮助。
先用可复现条件测量
浏览器缓存会让第二次刷新显得很快。为了比较修改前后,我固定了条件:打开 Disable cache;使用相同网络节流;硬刷新;记录 DOMContentLoaded、Load 和最大资源结束时间;每种方案跑三次取中位数。
我没有追求一个漂亮的 Lighthouse 分数,只回答三个问题:首屏必要资源何时到齐;用户什么时候能看到主要内容;哪一个请求占据最长关键路径。
图片优化比删循环有效得多
那张 5 MB 图片最终被裁到实际展示尺寸并压缩,体积降到几百 KB。更重要的是,我给图片写了明确尺寸,避免加载完成后页面整体跳动:
<img
src="/images/workbench-960.jpg"
width="960"
height="540"
alt="工作台界面"
>现代项目会使用 srcset、WebP/AVIF 和图片服务,但原则没有变化:传输的像素应该接近用户实际能看到的像素;布局在资源到达前就应该知道尺寸。
第三方资源也属于自己的性能预算
我曾觉得 CDN 上的脚本“不占自己服务器带宽”,就可以放心引入。Network 面板告诉我,用户仍然要为新域名的 DNS、TLS 和下载付费,而且第三方可用性不受我控制。
后来我给外部资源留下清单:用途、所有者、加载阶段、失败后影响和移除条件。一个只用于小动画的库,如果阻塞首屏,它的成本就高于功能价值。
资源: analytics.example/sdk.js
是否首屏必要: 否
加载策略: defer + 页面可交互后加载
超时影响: 放弃统计,不阻塞业务
负责人: growth请求完成不代表页面可用
Network 面板只能告诉我资源何时到达。脚本下载后还要解析执行,数据返回后还要渲染。为了不把网络与主线程混在一起,我后来同时看 Performance 面板:如果请求很快但页面仍卡住,就继续检查长任务、布局和绘制。
这次优化最有价值的变化,是我不再用“页面慢”概括所有问题。先把时间线拆开,再对症处理:传输大就减体积,连接多就合并域名或复用,TTFB 长就看服务端,主线程忙就看执行。一个模糊抱怨只有变成分段数据,才会变成工程问题。