优化之前,我的博客在移动网络下的首屏时间大概是 3.2 秒。这个数字不算灾难,但每次在外面打开自己的站点,都能感觉到那一下停顿。

我给自己定了一个不算激进的目标:首屏进入 1.5 秒以内,同时不牺牲图片质量和阅读体验。最后稳定在 1.1 秒左右。这篇文章记录中间做了什么,以及哪些做法被我否掉了。

先建立基线

优化最怕“凭感觉”。我先固定了测量条件:

  • 同一台手机、同一张 SIM 卡、同一地点;
  • Chrome 无痕模式,禁用缓存;
  • Lighthouse 移动端模拟,跑三次取中位数;
  • 同时记录真实网络下的首屏时间。

基线数据:LCP 3.2s,总传输 2.4MB,请求 38 个,其中图片 1.9MB。问题很明显——页面主要重量在图片。

图片:最大的一块

第一张文章封面原图是 1.8MB 的 JPEG,直接放在列表页。它在 360px 宽的屏幕上显示,却按 2400px 的尺寸下载。这是最典型的浪费。

我做了四件事:

  1. 按显示尺寸生成多档图片,用 srcset 让浏览器自己选;
  2. 转成 WebP,在质量 78 左右,体积约为原 JPEG 的 30%;
  3. 首屏之外的图片加 loading="lazy",减少初始请求;
  4. 给图片写死宽高比,避免加载后布局跳动。

结果:图片总传输从 1.9MB 降到 420KB,LCP 从 3.2s 降到 1.9s。

这里有一个取舍:WebP 在老设备上的兼容性。我保留了 JPEG 作为 <picture> 的 fallback,代价是构建时多生成一套文件。对现在的浏览器覆盖率来说,这个代价可以接受。

字体:一个容易被忽略的阻塞点

页面用了一款衬线字体,通过外部字体服务加载。它的 CSS 在 <head> 里,字体文件又在 CSS 之后才请求,等于多了一轮串行等待。

我做了两个调整:

  • 把字体文件放到自己的域名下,减少一次 DNS 和连接;
  • 用 font-display: swap,让文字先用系统字体显示,字体到了再替换。

同时我只保留实际用到的字重和字符集。中文字体文件很大,全量加载不现实,所以正文最终回退到系统宋体/黑体,装饰性标题才用自定义字体。

结果:字体相关的阻塞消失,LCP 再降 300ms 左右。代价是字体替换时有一次轻微跳动,我用相近的系统字体和固定的行高把跳动压到几乎看不出来。

JavaScript:删掉比优化更快

我检查了页面上加载的脚本,发现两个库只在很偶尔的交互里用到,却阻塞了整个页面的解析。

处理方式很直接:删掉一个,另一个改成 defer,并把初始化逻辑放到 DOMContentLoaded 之后。首屏不再等待它们。

这一步没有复杂的技巧,但收益很大。很多时候“优化 JS”最好的方式不是压缩和拆包,而是确认它是否真的需要在首屏加载。

CSS:只内联关键部分

我试过把全部 CSS 内联进 HTML,结果 HTML 变大,缓存失效,后续页面反而更慢。

最后只内联了首屏需要的少量规则,其余样式走外部文件并长缓存。这个边界不好找,我的判断标准是:首屏渲染前必须知道的样式才内联,其余都可以等。

CDN 与协议

图片和静态资源走 CDN,开启了 HTTP/2。这两项不是这次优化的主角,但它们让前面的改动更容易生效:静态资源命中缓存后,首屏请求少了很多往返。

我没有开 HTTP/3,也没有做服务端渲染。对静态博客来说,收益不足以抵消额外的复杂度。

结果与代价

最终数据:

指标 优化前 优化后
LCP 3.2s 1.1s
总传输 2.4MB 680KB
请求数 38 21
首屏图片 1.8MB 约 180KB

代价也有:

  • 构建时间变长,因为要生成多档图片;
  • 图片目录多了一套 WebP 文件,备份体积增加;
  • 字体回退让部分标题在不同系统上观感略有差异;
  • 缓存规则更复杂,发布时要记得刷新。

这些代价我都能接受,因为它们不影响内容,也不会让故障更难恢复。

一点反思

这次优化最有效的几步,都是“删掉不必要的东西”,而不是“用更高级的技术”。图片按需加载、字体不阻塞、脚本延后,这些做法十年前就存在。

真正花时间的不是执行,而是测量和确认:确认哪一块最重、确认改动真的有效、确认没有把别的地方弄坏。优化不是一场技术表演,它更像整理房间——先看清楚东西在哪,再决定扔什么。