PERFORMANCE

首屏从 4.2s 到 1.1s:
一次前端性能优化复盘

不追求漂亮的分数,只关心真实用户等了多久。从测量、定位到验证的一次完整优化过程。

这是一次内容型产品首页的性能优化。上线初期,页面在公司网络和我的高性能笔记本上“感觉还行”,但在真实用户数据里,移动端 LCP 的 p75 达到了 4.2 秒。最危险的不是慢,而是我们一直用错误的环境评估它。

这次复盘不打算罗列所有优化清单,而是还原一条更重要的路径:如何从一组指标,定位到具体原因,再验证改动是否真的有效。

先把“慢”定义清楚

性能问题最怕从解决方案开始。有人会说“上 CDN”“换框架”“图片压缩”,这些都可能是答案,但前提是知道问题发生在哪个阶段。

我们同时采集两类数据:

  • 实验室数据:Lighthouse 和 Performance 面板,固定网络与 CPU 降速,用来稳定复现和定位。
  • 现场数据:真实用户的 LCP、INP、CLS 和资源加载瀑布,按设备、地区与网络分层,用来判断改动是否真正改善了体验。

最初基线如下。所有数字都来自同一套采样策略,而不是挑出来的最好或最差样本。

LCP / p754.2s移动端现场数据
INP / p75340ms首访与二次访问混合
JS / gzip520KB首屏必需代码

数据透露了两个信号:LCP 明显超标,并且 JavaScript 体积很大。它们未必是同一个问题,但很可能共享同一个根因——页面在首屏之前做了太多不必要的事。

找到真正拖慢首屏的东西

打开 Performance 面板后,最显眼的是主线程上一大段密集的脚本执行。但 LCP 元素迟迟没有出现,说明问题不只是“JS 慢”,而是关键内容被资源加载和脚本执行共同卡住了。

根因一:LCP 图片被当成普通图片

首屏主图是一张 1920px 宽的 JPEG,文件大小约 1.9MB。它没有固定宽高,没有响应式候选,在低端机上下载和解码都很吃力。更糟的是,图片下面还有一层轮播初始化逻辑,只有等 JS 加载执行后才真正显示。

根因二:为“以后可能会用”付出的脚本成本

主包里同时引入了图标库、日期格式化库、轮播插件、全站埋点 SDK 和一个用于非首屏模块的富文本渲染器。它们被打进同一个入口,阻碍了首屏交互。真正的问题不是某个库不好,而是它们进入首屏的时机不对。

根因三:第三方脚本抢占了关键路径

热力图和客服组件的脚本位于 <head> 内,在低端网络下平均消耗 600ms 以上。它们对首屏内容没有任何贡献,却和主内容争抢带宽与主线程。

性能优化的核心不是让每一件事都变快,而是让不该出现在关键路径上的事情先离开。

— 本次优化最重要的判断

分阶段处理关键路径

1. 把 LCP 资源变成真正的优先资源

为图片建立尺寸约束,并生成 AVIF 与 WebP 候选。首屏主图使用 fetchpriority="high",非首屏图片全部延迟加载。注意,只对确认的 LCP 候选做高优先级提示,滥用在所有图片上会稀释信号。

hero-media.html
<picture>
  <source
    type="image/avif"
    srcset="hero-640.avif 640w, hero-1280.avif 1280w"
    sizes="(max-width: 768px) 100vw, 60vw">
  <source
    type="image/webp"
    srcset="hero-640.webp 640w, hero-1280.webp 1280w"
    sizes="(max-width: 768px) 100vw, 60vw">
  <img
    src="hero-1280.jpg"
    width="1280" height="720"
    fetchpriority="high"
    decoding="async"
    alt="...">
</picture>

同时,我们移除了“图片加载完才显示”的逻辑。图片应该在 HTML 解析时就成为页面的一部分,而不是等某个组件挂载后才获得出场资格。

2. 按首屏价值拆分 JavaScript

我们以“首屏可见且可交互”为界线重新组织入口:

  • 日期格式化改用原生 Intl.DateTimeFormat,移除对应依赖;
  • 图标改为按需加载的 SVG,只保留首屏实际使用的图标;
  • 轮播改为 CSS 首屏呈现、交互后再增强,避免 JS 阻塞内容出现;
  • 富文本渲染器、编辑器等只在对应路由动态导入。

优化后首屏入口的 gzip 体积从 520KB 降到 178KB。更关键的指标不是体积,而是长任务从约 2.8 秒降到 0.6 秒以内,用户更早能够与页面交互。

3. 第三方脚本延后,但保留兜底

客服与热力图脚本被移动到用户空闲时加载。客服入口先展示一个占位按钮,点击时再按需初始化真正组件;如果加载失败,仍保留电话与邮件等基础路径。延后不是消失,功能可用性必须被明确接管。

4. 字体与 CSS 同样属于首屏资源

我们把四个字重裁剪为首屏需要的两个子集,采用 font-display: swap,并为系统字体回退设置尽量接近的行高与字间距。CSS 方面则确保首屏样式尽早到达,非关键模块样式随路由加载。视觉稳定性通过尺寸占位和字体回退校准共同控制。

优化之后,必须验证

所有改动完成后,我们没有立刻宣布完成,而是进行了两周的对比观察。实验和对照使用相同的设备条件和采样口径,重点看分布而不是平均值。

FIELD LCP / p751.9s移动端,两周窗口
TBT / LAB-78%相比优化前
INP / p75190ms首访与二次访问混合

实验室 LCP 从 4.2 秒降到了 1.1 秒,但现场 p75 是 1.9 秒。这个差距很正常:实验室是固定条件,现实世界有更多网络、设备与缓存差异。对外沟通时,我们只使用现场 p75 描述改善,不用实验室成绩制造更强的结论。

我们还用 RUM 继续监控回归。性能不是一次性项目,发版、运营活动和第三方接入都可能让已经优化的路径重新变慢。

web-vitals.js
import { onCLS, onINP, onLCP } from 'web-vitals';

const report = (metric) => {
  navigator.sendBeacon(
    '/rum',
    JSON.stringify({
      name: metric.name,
      value: metric.value,
      rating: metric.rating,
      path: location.pathname,
    })
  );
};

onLCP(report);
onINP(report);
onCLS(report);

这次复盘留下的四条原则

  1. 先有基线,再谈优化。没有可比较的数据,优化就只是一组个人偏好。
  2. 区分关键路径和非关键路径。不是所有功能都要首屏加载,但所有被延后的功能都要有清晰的交互兜底。
  3. 把指标当作诊断工具,不要当作奖品。Lighthouse 100 分不等于用户体感优秀,低分也不总能直接说明根因。
  4. 优化之后继续监控。性能预算应该进入构建和发布流程,而不是靠某次专项优化永久解决。

1.1 秒不是重点

标题里的 1.1 秒只是一个实验室结果,真正重要的收益发生在看不见的地方:更少的首屏脚本、更清晰的功能边界、更可靠的发布约束,以及团队终于用同一套数据讨论体验。

性能优化的终点从来不是某个数字,而是一个更难被破坏的系统。

沈健健

前端工程师,关注界面工程、性能与设计系统。相信好的技术判断,最终都会反映在用户不必思考的细节里。