Web 性能优化实战:从 Core Web Vitals 到工程化性能治理

Web 性能优化实战:从 Core Web Vitals 到工程化性能治理

Web 性能优化并不是简单地压缩几张图片、删除几个 JavaScript 文件,而是围绕加载速度、交互响应、视觉稳定性和资源成本建立一套可测量、可分析、可持续的工程体系。

目录

  1. 为什么需要进行 Web 性能优化
  2. Web 页面从请求到展示经历了什么
  3. Web 性能核心指标
  4. 实验室数据与真实用户数据
  5. 建立性能基线与性能预算
  6. 优化服务器响应时间与 TTFB
  7. 优化网络请求和资源加载链路
  8. LCP 优化实战
  9. 图片性能优化
  10. CSS 性能优化
  11. Web 字体优化
  12. JavaScript 加载与体积优化
  13. INP 与主线程优化
  14. 渲染性能优化
  15. CLS 与页面稳定性优化
  16. SPA、SSR、SSG 与水合性能
  17. 浏览器缓存与 CDN 优化
  18. 第三方脚本优化
  19. 前端性能监控与 RUM
  20. 使用 Lighthouse CI 建立性能门禁
  21. Web 性能优化实战案例
  22. 常见性能问题排查方法
  23. Web 性能上线检查清单
  24. 常见问题
  25. 总结

1. 为什么需要进行 Web 性能优化

Web 性能直接影响用户体验、业务转化率、搜索引擎排名和服务器成本。

当页面加载缓慢时,用户可能会遇到以下问题:

  • 页面长时间白屏;
  • 首屏图片迟迟不能显示;
  • 点击按钮后没有反馈;
  • 输入文字时页面卡顿;
  • 页面内容突然跳动;
  • 手机端耗电、发热明显;
  • 弱网环境下页面无法正常使用。

从业务角度看,性能问题通常会进一步导致:

  • 跳出率升高;
  • 页面停留时间下降;
  • 注册、下单、支付转化率下降;
  • 搜索引擎自然流量减少;
  • CDN 和服务器带宽成本增加;
  • 客服投诉和线上故障增多。

因此,Web 性能优化的目标不应该只是"让 Lighthouse 跑到 100 分",而应该是:

  1. 让用户更快看到主要内容;
  2. 让用户操作后更快得到反馈;
  3. 保证页面展示过程稳定;
  4. 控制资源体积和请求数量;
  5. 在真实用户环境中持续监控性能;
  6. 防止新版本引入性能回退。

2. Web 页面从请求到展示经历了什么

要进行性能优化,首先要理解页面加载链路。

用户在浏览器中输入 URL 后,大致会经历以下过程:

text 复制代码
输入 URL
   ↓
解析 URL
   ↓
检查浏览器缓存
   ↓
DNS 查询
   ↓
建立 TCP 或 QUIC 连接
   ↓
建立 TLS 安全连接
   ↓
发送 HTTP 请求
   ↓
服务器处理请求
   ↓
返回 HTML
   ↓
浏览器解析 HTML
   ↓
下载 CSS、JavaScript、图片、字体
   ↓
构建 DOM 和 CSSOM
   ↓
生成渲染树
   ↓
布局 Layout
   ↓
绘制 Paint
   ↓
栅格化 Raster
   ↓
合成 Composite
   ↓
页面显示

页面性能问题可能出现在任何一个环节。

例如:

性能现象 可能原因
页面长时间白屏 TTFB 高、CSS 阻塞、客户端渲染依赖大包 JavaScript
首屏大图加载慢 图片过大、发现时间晚、请求优先级低
点击按钮卡顿 主线程存在长任务、事件回调执行时间过长
页面不断跳动 图片没有尺寸、广告位没有预留空间、字体切换
二次访问仍然很慢 缓存策略不合理、静态资源没有内容哈希
低端手机卡顿 JavaScript 解析执行量过大、DOM 规模过大
海外访问缓慢 CDN 节点不足、跨区域回源、连接链路过长

性能优化的关键,就是先找到时间究竟消耗在什么地方,然后针对瓶颈进行优化。


3. Web 性能核心指标

3.1 Core Web Vitals

Core Web Vitals 是衡量用户体验的三个核心指标:

指标 含义 良好 需要改进 较差
LCP 最大内容绘制,衡量主要内容加载速度 ≤ 2.5 秒 2.5~4 秒 > 4 秒
INP 与下一次绘制的交互,衡量整体交互响应性 ≤ 200 毫秒 200~500 毫秒 > 500 毫秒
CLS 累积布局偏移,衡量视觉稳定性 ≤ 0.1 0.1~0.25 > 0.25

这些指标应当按照真实用户访问数据的第 75 百分位进行评估。

第 75 百分位可以简单理解为:至少 75% 的用户访问体验都需要达到目标,而不是只保证平均值看起来正常。

3.2 LCP

LCP 的全称是 Largest Contentful Paint,即最大内容绘制。

它关注首屏中最大图片、视频封面或文本块完成绘制的时间。

常见的 LCP 元素包括:

  • 首页头图;
  • 商品主图;
  • Banner 图片;
  • 文章标题区域;
  • 大段首屏文本;
  • 视频封面。

LCP 可以拆成四部分:

text 复制代码
LCP =
    TTFB
  + 资源加载延迟
  + 资源加载耗时
  + 元素渲染延迟

这意味着图片压缩并不是 LCP 优化的全部。

如果图片已经下载完成,但页面必须等待 JavaScript 执行后才能显示,那么瓶颈实际上是元素渲染延迟。

3.3 INP

INP 的全称是 Interaction to Next Paint。

它衡量用户进行点击、触摸或键盘输入后,页面完成下一次视觉更新需要多长时间。

一次交互可以拆成三个阶段:

text 复制代码
输入延迟
   +
事件处理时间
   +
展示延迟

常见 INP 问题包括:

  • 用户点击时主线程正在执行其他任务;
  • 事件回调中进行了大量计算;
  • 更新了规模过大的 DOM;
  • 框架一次性重新渲染大量组件;
  • 页面布局和绘制开销过大;
  • 第三方脚本长期占用主线程。

3.4 CLS

CLS 的全称是 Cumulative Layout Shift,即累积布局偏移。

常见问题包括:

  • 图片没有声明宽高;
  • 广告或推荐位没有预留空间;
  • 接口返回后在页面顶部插入新内容;
  • Web 字体加载后文字尺寸发生变化;
  • 动画修改了 widthheighttopleft
  • 骨架屏和真实内容尺寸不一致。

3.5 其他诊断指标

除了 Core Web Vitals,还应关注以下指标:

指标 作用
TTFB 从发起导航到收到 HTML 第一个字节的时间
FCP 页面首次绘制文字、图片或其他内容的时间
TBT 实验室环境中主线程被长任务阻塞的总时间
Speed Index 页面可视内容完成展示的速度
Resource Timing 每个资源的 DNS、连接、等待、下载耗时
Long Task 主线程中执行时间超过 50 毫秒的任务
JS Heap JavaScript 堆内存使用量
DOM Size 页面 DOM 节点数量和层级

需要注意,Lighthouse 无法在一次短暂的页面加载测试中完整模拟用户整个访问周期的 INP,通常会使用 TBT 等指标辅助诊断主线程阻塞问题。


4. 实验室数据与真实用户数据

Web 性能数据主要分为实验室数据和真实用户数据。

4.1 实验室数据

实验室数据是在可控环境下运行测试得到的数据。

常用工具包括:

  • Chrome DevTools;
  • Lighthouse;
  • PageSpeed Insights 的实验室部分;
  • WebPageTest;
  • Lighthouse CI。

实验室数据适合:

  • 在开发阶段复现问题;
  • 分析网络瀑布图;
  • 查看主线程任务;
  • 对比优化前后结果;
  • 在 CI 中检测性能回退。

它的局限是无法完整代表真实用户。

开发者电脑通常性能较强、网络较快,与低端手机和弱网用户差别很大。

4.2 真实用户数据

真实用户监控通常称为 RUM,也就是 Real User Monitoring。

RUM 数据来自真实用户的:

  • 实际设备;
  • 实际网络;
  • 实际地理位置;
  • 实际登录状态;
  • 实际页面内容;
  • 实际操作路径。

真实用户数据适合回答:

  • 哪些页面的 LCP 最差?
  • 哪些设备的 INP 较高?
  • 某次发布是否导致性能回退?
  • 低端手机是否明显比高端手机慢?
  • 某个地区是否存在 CDN 或回源问题?

4.3 推荐的性能工作流

text 复制代码
真实用户数据发现问题
        ↓
按页面、设备、网络、版本进行分组
        ↓
在实验室环境中复现
        ↓
使用 Network 和 Performance 定位原因
        ↓
实施优化
        ↓
本地和 CI 验证
        ↓
灰度发布
        ↓
继续观察真实用户数据

不要只根据单次 Lighthouse 分数做结论。

性能测试存在波动,应当多次运行,并结合资源体积、请求数量、CPU 时间和真实用户数据综合判断。


5. 建立性能基线与性能预算

5.1 建立性能基线

在优化之前,先记录当前数据。

建议至少记录:

维度 示例
页面 首页、列表页、详情页、登录页、结算页
设备 高端手机、低端手机、桌面设备
网络 Wi-Fi、4G、弱网
用户状态 未登录、已登录、新用户、老用户
缓存状态 首次访问、二次访问
页面指标 TTFB、FCP、LCP、INP、CLS
资源指标 JS、CSS、图片、字体体积
运行指标 长任务、DOM 节点、内存

5.2 设置性能预算

性能预算是项目允许消耗的最大性能资源。

下面是一份示例预算,实际项目应根据用户群体和现有基线调整:

json 复制代码
{
  "page": "product-detail",
  "budgets": {
    "lcpMs": 2500,
    "inpMs": 200,
    "cls": 0.1,
    "initialJavaScriptKb": 300,
    "initialCssKb": 100,
    "initialImageKb": 500,
    "fontKb": 150,
    "requestCount": 60
  }
}

性能预算不要只设置 Lighthouse 总分。

总分可能随着工具权重变化而变化,而以下指标更加直接:

  • JavaScript 体积;
  • 图片体积;
  • 请求数量;
  • LCP 资源开始加载的时间;
  • 主线程执行时间;
  • 长任务数量;
  • TTFB;
  • LCP;
  • CLS。

5.3 区分冷启动和热缓存

测试时需要分别测量:

  1. 首次访问,没有可用缓存;
  2. 再次访问,静态资源命中缓存;
  3. 从其他页面进行站内跳转;
  4. 浏览器前进后退缓存恢复。

如果只测试热缓存,可能会掩盖新用户首访性能问题。


6. 优化服务器响应时间与 TTFB

TTFB 是整个页面加载链路的起点。

如果 HTML 返回就花了 3 秒,那么页面很难达到 2.5 秒以内的 LCP。

6.1 TTFB 较高的常见原因

  • DNS 解析慢;
  • 网络距离过远;
  • 多次重定向;
  • TLS 建连时间过长;
  • 应用启动慢;
  • 数据库查询慢;
  • 串行调用多个下游服务;
  • 模板渲染耗时;
  • 缓存未命中;
  • CDN 回源慢;
  • 服务端资源不足;
  • 服务端进行过多同步计算。

6.2 减少重定向

不推荐:

text 复制代码
http://example.com
    → https://example.com
    → https://www.example.com
    → https://www.example.com/home

如果业务允许,应尽量直接跳转到最终地址。

6.3 优化服务端处理

可以从以下方面入手:

  • 为高频查询建立合适的数据库索引;
  • 避免循环查询导致的 N+1 问题;
  • 将互不依赖的下游请求并行执行;
  • 缓存公共页面和公共接口;
  • 对热点数据进行预热;
  • 避免在请求链路执行不必要的同步任务;
  • 为服务端调用设置合理的超时和降级策略;
  • 通过 Server-Timing 返回服务端阶段耗时。

示例:

http 复制代码
Server-Timing: db;dur=42, cache;dur=3, app;dur=68

浏览器可以在开发者工具中展示这些数据,从而帮助判断时间究竟消耗在数据库、缓存还是应用逻辑中。

6.4 使用流式响应

对于服务端渲染页面,可以在条件允许时逐步输出 HTML。

浏览器收到部分 HTML 后就可以开始:

  • 解析文档;
  • 发现 CSS;
  • 发现首屏图片;
  • 预加载关键资源。

但是流式输出不能弥补严重的后端性能问题。首字节仍然应该尽早返回。


7. 优化网络请求和资源加载链路

7.1 分析 Network 瀑布图

重点检查:

  • 哪个请求最早开始;
  • 哪个请求阻塞时间最长;
  • LCP 资源什么时候被发现;
  • 是否存在重复资源;
  • 是否出现长重定向链;
  • 请求是否等待连接;
  • 是否存在体积过大的 JavaScript;
  • 是否有接口串行依赖;
  • 静态资源是否命中缓存。

一个典型的低效链路可能是:

text 复制代码
HTML
  ↓
main.js
  ↓
执行 JavaScript
  ↓
请求页面数据
  ↓
拼接图片地址
  ↓
请求首屏图片
  ↓
显示 LCP 元素

更好的链路是:

text 复制代码
HTML ──────────────→ 页面结构
  ├───────────────→ 关键 CSS
  ├───────────────→ 首屏图片
  └───────────────→ JavaScript

首屏关键资源应尽早出现在初始 HTML 中,让浏览器的预加载扫描器能够及时发现。

7.2 减少不必要的串行依赖

不推荐:

javascript 复制代码
const user = await fetchUser();
const products = await fetchProducts();
const recommendations = await fetchRecommendations();

如果这些请求互不依赖,可以并行执行:

javascript 复制代码
const [user, products, recommendations] = await Promise.all([
  fetchUser(),
  fetchProducts(),
  fetchRecommendations()
]);

如果推荐内容不是首屏关键内容,可以延后加载:

javascript 复制代码
const [user, products] = await Promise.all([
  fetchUser(),
  fetchProducts()
]);

renderMainContent({ user, products });

requestIdleCallback(() => {
  fetchRecommendations().then(renderRecommendations);
});

requestIdleCallback 并不是所有环境都具有一致支持,生产项目应进行兼容性检查,或者使用框架调度机制和可控的延迟任务。

7.3 合理使用资源提示

preconnect

提前建立到关键跨域服务器的连接:

html 复制代码
<link rel="preconnect" href="https://static.example.com" crossorigin>

适用于:

  • 核心静态资源域名;
  • 首屏字体域名;
  • 首屏关键 API 域名。

不要对大量域名使用 preconnect,因为 DNS、TCP、TLS 都会消耗资源。

dns-prefetch

只提前进行 DNS 查询:

html 复制代码
<link rel="dns-prefetch" href="//analytics.example.com">
preload

提前加载当前页面一定会使用的关键资源:

html 复制代码
<link
  rel="preload"
  href="/assets/hero.avif"
  as="image"
  type="image/avif"
  fetchpriority="high"
>

错误使用 preload 可能会抢占其他关键资源带宽,因此只能用于少量、确定需要的资源。

prefetch

低优先级获取未来可能使用的资源:

html 复制代码
<link rel="prefetch" href="/assets/next-page.js">

浏览器可能根据网络状况忽略预取提示,因此业务逻辑不能依赖资源一定被预取。

7.4 不要机械地合并所有资源

HTTP/2 和 HTTP/3 支持多路复用,不再需要像 HTTP/1.1 时代那样无条件把所有文件合并成一个超大文件。

过度合并可能导致:

  • 首屏下载不需要的代码;
  • 修改一行代码导致整个大文件缓存失效;
  • 无法按路由拆分;
  • 解析和执行成本升高。

正确方向是:

  • 首屏只加载必要资源;
  • 公共依赖合理拆包;
  • 页面功能按需加载;
  • 避免产生大量细碎且无意义的小请求;
  • 通过瀑布图验证真实效果。

8. LCP 优化实战

LCP 优化应按照以下顺序分析:

text 复制代码
TTFB
  ↓
LCP 资源发现时间
  ↓
LCP 资源下载时间
  ↓
LCP 元素渲染时间

8.1 让 LCP 资源尽早被发现

不推荐通过 JavaScript 动态创建首屏图片:

javascript 复制代码
const image = new Image();
image.src = '/images/hero.avif';
document.querySelector('.hero').appendChild(image);

浏览器必须先下载并执行 JavaScript,之后才能发现图片。

更好的方式是直接写入 HTML:

html 复制代码
<img
  src="/images/hero.avif"
  width="1440"
  height="720"
  fetchpriority="high"
  alt="产品展示"
>

8.2 不要懒加载 LCP 图片

错误示例:

html 复制代码
<img
  src="/images/hero.avif"
  loading="lazy"
  alt="首屏主图"
>

首屏图片尤其是 LCP 图片,应使用默认的立即加载行为,并可以根据测量结果设置:

html 复制代码
<img
  src="/images/hero.avif"
  width="1440"
  height="720"
  fetchpriority="high"
  alt="首屏主图"
>

不要给大量图片设置 fetchpriority="high",否则所有资源都变成高优先级,也就失去了优先级提示的意义。

8.3 减少 LCP 元素渲染延迟

即使 LCP 图片已经下载完成,也可能因为以下原因无法显示:

  • 主线程正在执行大型 JavaScript;
  • 元素默认被设置为 display: none
  • 页面等待接口完成后才整体显示;
  • 客户端渲染尚未完成;
  • Web 字体阻塞文本绘制;
  • CSS 动画延迟元素出现。

不推荐:

css 复制代码
.page {
  opacity: 0;
}

.page.ready {
  opacity: 1;
}

如果 .ready 必须等待整个应用初始化完成才添加,那么首屏内容也会被一起延迟。

应当优先展示已经可以展示的页面结构和主要内容,再逐步增强交互功能。


9. 图片性能优化

图片往往是页面中占用带宽最多的资源。

9.1 选择合适的图片格式

格式 适用场景
JPEG 普通照片,兼容性好
PNG 需要透明背景或无损细节的图片
WebP 照片、透明图、动图,压缩效率较好
AVIF 对体积要求较高的照片和复杂图片
SVG 图标、Logo、简单矢量图形

格式选择不能只看扩展名,还要比较:

  • 文件体积;
  • 解码开销;
  • 清晰度;
  • 浏览器兼容性;
  • 是否需要透明或动画。

9.2 使用响应式图片

不要让宽度只有 390 像素的手机直接下载一张 3000 像素宽的图片。

html 复制代码
<img
  src="/images/product-800.webp"
  srcset="
    /images/product-480.webp 480w,
    /images/product-800.webp 800w,
    /images/product-1200.webp 1200w
  "
  sizes="
    (max-width: 600px) 100vw,
    (max-width: 1200px) 50vw,
    600px
  "
  width="1200"
  height="800"
  alt="商品图片"
>

srcset 描述候选图片,sizes 告诉浏览器图片在不同视口中的预计显示宽度。

9.3 使用 picture 提供多种格式

html 复制代码
<picture>
  <source
    srcset="/images/banner.avif"
    type="image/avif"
  >
  <source
    srcset="/images/banner.webp"
    type="image/webp"
  >
  <img
    src="/images/banner.jpg"
    width="1440"
    height="720"
    fetchpriority="high"
    alt="活动 Banner"
  >
</picture>

9.4 懒加载首屏外图片

html 复制代码
<img
  src="/images/recommendation.webp"
  width="600"
  height="400"
  loading="lazy"
  decoding="async"
  alt="推荐内容"
>

适合懒加载的内容包括:

  • 首屏之外的图片;
  • 长列表中的商品图;
  • 评论区头像;
  • 页脚图片;
  • 非首屏 iframe。

不要懒加载:

  • LCP 图片;
  • 首屏关键 Logo;
  • 首屏立即可见的商品主图。

9.5 始终声明图片尺寸

html 复制代码
<img
  src="/images/product.webp"
  width="800"
  height="600"
  alt="商品图片"
>

响应式布局可以继续使用:

css 复制代码
img {
  max-width: 100%;
  height: auto;
}

HTML 中的 widthheight 可以帮助浏览器提前计算宽高比、预留空间,从而减少 CLS。


10. CSS 性能优化

10.1 CSS 为什么会阻塞渲染

浏览器通常需要完成以下过程:

text 复制代码
解析 HTML
   +
解析 CSS
   ↓
构建 DOM 和 CSSOM
   ↓
生成渲染树
   ↓
布局和绘制

如果关键 CSS 下载很慢,页面可能长时间无法正常绘制。

10.2 删除无用 CSS

大型组件库、历史样式和重复样式可能导致 CSS 文件不断膨胀。

可以通过构建工具分析:

  • 哪些 CSS 没有被页面使用;
  • 哪些组件样式被全量引入;
  • 是否存在重复规则;
  • 是否可以按页面拆分;
  • 是否存在仅后台页面使用但首页也加载的样式。

删除 CSS 时要注意动态类名,例如:

javascript 复制代码
const className = `status-${status}`;

静态扫描工具可能无法识别这类动态生成的类名,需要配置保留列表。

10.3 谨慎内联关键 CSS

将少量首屏关键 CSS 内联到 HTML 中,可以减少关键请求链。

html 复制代码
<style>
  .page-header {
    min-height: 64px;
    display: flex;
    align-items: center;
  }

  .hero {
    aspect-ratio: 2 / 1;
    background: #f5f7fa;
  }
</style>

但不要把整个 CSS 文件都内联,否则会:

  • 增大 HTML;
  • 影响 CSS 缓存复用;
  • 增加服务端输出体积;
  • 增加内容安全策略管理难度。

10.4 优化动画属性

优先使用:

css 复制代码
.card {
  transition:
    transform 200ms ease,
    opacity 200ms ease;
}

.card:hover {
  transform: translateY(-4px);
  opacity: 0.95;
}

谨慎使用会频繁触发布局的属性:

css 复制代码
.card:hover {
  top: -4px;
  width: 420px;
}

transformopacity 通常更适合动画,但不代表所有此类动画都没有成本。

will-change 也不能全局滥用:

css 复制代码
.animating-element {
  will-change: transform;
}

长期给大量元素设置 will-change 会增加内存和合成层成本。应只在确实需要时使用,并在动画结束后移除。

10.5 延迟渲染屏幕外内容

对于很长的页面,可以尝试:

css 复制代码
.article-section {
  content-visibility: auto;
  contain-intrinsic-size: auto 600px;
}

content-visibility: auto 可以让浏览器暂时跳过屏幕外内容的布局和绘制。

使用前需要验证:

  • 滚动体验;
  • 页面内搜索;
  • 锚点跳转;
  • 辅助技术表现;
  • 目标浏览器兼容性。

11. Web 字体优化

字体文件可能同时影响 LCP 和 CLS。

11.1 只加载实际需要的字体

不要一次加载:

  • 多个字体家族;
  • 所有字重;
  • 所有字符集;
  • 页面根本没有使用的斜体字体。

例如页面只使用 400 和 600 字重,就不应加载 100~900 的所有独立字体文件。

11.2 使用 WOFF2

WOFF2 通常是现代 Web 字体的优先选择。

css 复制代码
@font-face {
  font-family: "AppSans";
  src: url("/fonts/app-sans-regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

11.3 合理设置 font-display

常见取值包括:

  • swap:先显示后备字体,再替换为 Web 字体;
  • optional:网络条件不好时可能继续使用后备字体;
  • fallback:在短时间等待后使用后备字体。

font-display: swap 可以减少文字长时间不可见的问题,但字体切换时可能产生布局变化。

因此还需要:

  • 选择尺寸接近的后备字体;
  • 使用字体度量覆盖属性;
  • 只预加载首屏真正需要的字体;
  • 对字体进行子集化。

11.4 预加载关键字体

html 复制代码
<link
  rel="preload"
  href="/fonts/app-sans-regular.woff2"
  as="font"
  type="font/woff2"
  crossorigin
>

即使字体和页面同源,字体预加载通常也需要正确处理 crossorigin,否则可能出现重复请求。

不要预加载所有字体,只预加载首屏确定需要的字体。


12. JavaScript 加载与体积优化

JavaScript 的性能成本不只是下载。

浏览器还需要:

text 复制代码
下载
  ↓
解压
  ↓
解析
  ↓
编译
  ↓
执行
  ↓
创建对象和事件
  ↓
触发框架渲染

相同体积的 JavaScript 和图片,对低端设备造成的影响完全不同。

图片主要消耗网络和解码资源,而 JavaScript 还会占用主线程。

12.1 正确使用 async、defer 和 module

普通脚本:

html 复制代码
<script src="/js/app.js"></script>

可能阻塞 HTML 解析。

使用 defer

html 复制代码
<script src="/js/app.js" defer></script>

特点:

  • 与 HTML 并行下载;
  • 等 HTML 解析完成后执行;
  • 多个 defer 脚本通常按文档顺序执行。

使用 async

html 复制代码
<script src="/js/analytics.js" async></script>

特点:

  • 与 HTML 并行下载;
  • 下载完成后尽快执行;
  • 多个脚本执行顺序不确定。

适合独立统计脚本,不适合存在执行顺序依赖的业务脚本。

ES Module:

html 复制代码
<script type="module" src="/js/app.js"></script>

模块脚本默认具有类似 defer 的行为。

12.2 按路由拆分代码

javascript 复制代码
const routes = [
  {
    path: '/dashboard',
    load: () => import('./pages/dashboard.js')
  },
  {
    path: '/settings',
    load: () => import('./pages/settings.js')
  }
];

用户访问首页时,不应该下载后台管理页面的全部代码。

12.3 按功能懒加载

javascript 复制代码
document
  .querySelector('#open-editor')
  .addEventListener('click', async () => {
    const { createEditor } = await import('./editor.js');
    createEditor();
  });

适合懒加载:

  • 富文本编辑器;
  • 图表库;
  • 地图 SDK;
  • PDF 预览器;
  • 视频编辑器;
  • 不常使用的弹窗功能。

12.4 Tree Shaking

推荐使用 ES Module 的具名导入:

javascript 复制代码
import { debounce } from './utils.js';

避免无条件引入整个大型工具库:

javascript 复制代码
import utils from './all-utils.js';

Tree Shaking 能否生效还取决于:

  • 依赖是否使用 ES Module;
  • 构建工具配置;
  • 包是否声明副作用;
  • 代码是否存在难以静态分析的动态行为。

不能只看到"开启 Tree Shaking"就认为无用代码已经被删除,应检查最终产物。

12.5 避免重复依赖

常见问题包括:

  • 主项目和子包加载不同版本的相同库;
  • 多个组件库分别携带相同工具库;
  • 同一个 polyfill 被重复注入;
  • 服务端渲染包和客户端包边界不清;
  • monorepo 依赖解析导致重复打包。

应使用构建产物分析工具查看依赖构成,而不是只看源码目录大小。

12.6 将大型计算移出主线程

Web Worker 适合:

  • 大量数据计算;
  • 文件解析;
  • 压缩和解压;
  • 图像处理;
  • 加密计算;
  • 大型 JSON 转换。

主线程:

javascript 复制代码
const worker = new Worker(
  new URL('./calculate.worker.js', import.meta.url),
  { type: 'module' }
);

worker.postMessage({ records });

worker.addEventListener('message', event => {
  renderResult(event.data);
});

Worker:

javascript 复制代码
self.addEventListener('message', event => {
  const { records } = event.data;
  const result = calculateStatistics(records);
  self.postMessage(result);
});

Web Worker 无法直接操作 DOM,数据传输也有成本,因此不适合所有任务。


13. INP 与主线程优化

13.1 什么是长任务

浏览器主线程除了执行 JavaScript,还需要处理:

  • 用户输入;
  • 样式计算;
  • 页面布局;
  • 绘制;
  • 垃圾回收;
  • 框架更新。

当一个任务执行时间超过 50 毫秒时,通常会被视为长任务。

如果用户恰好在长任务执行期间点击按钮,交互就必须等待。

13.2 拆分长任务

不推荐一次处理全部数据:

javascript 复制代码
function processAll(items) {
  for (const item of items) {
    performExpensiveWork(item);
  }
}

可以分批处理并主动让出主线程:

javascript 复制代码
async function yieldToMain() {
  if (
    'scheduler' in window &&
    typeof window.scheduler.yield === 'function'
  ) {
    await window.scheduler.yield();
    return;
  }

  await new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

async function processInChunks(items, chunkSize = 100) {
  for (let index = 0; index < items.length; index += chunkSize) {
    const chunk = items.slice(index, index + chunkSize);

    for (const item of chunk) {
      performExpensiveWork(item);
    }

    await yieldToMain();
  }
}

分片大小需要通过真实设备测试确定。

分片太大会继续阻塞主线程,分片太小则会增加调度开销。

13.3 尽早提供视觉反馈

用户点击"提交"后,应尽快显示状态:

javascript 复制代码
submitButton.addEventListener('click', async () => {
  submitButton.disabled = true;
  submitButton.textContent = '提交中...';

  await yieldToMain();

  try {
    await submitOrder();
    showSuccessMessage();
  } finally {
    submitButton.disabled = false;
    submitButton.textContent = '提交订单';
  }
});

先更新按钮状态,可以让用户更快知道操作已经生效。

13.4 减少单次渲染规模

不推荐一次渲染一万个列表节点:

javascript 复制代码
for (const item of items) {
  list.appendChild(createItemElement(item));
}

可以使用:

  • 分页;
  • 虚拟列表;
  • 分批渲染;
  • 服务端筛选;
  • 增量加载;
  • DocumentFragment
  • 框架提供的窗口化组件。

13.5 避免频繁强制同步布局

不推荐交替读取和修改布局:

javascript 复制代码
for (const element of elements) {
  element.style.width = `${container.offsetWidth / 2}px`;
}

读取 offsetWidth 后立即修改样式,可能反复触发布局计算。

可以先统一读取,再统一写入:

javascript 复制代码
const containerWidth = container.offsetWidth;
const targetWidth = `${containerWidth / 2}px`;

for (const element of elements) {
  element.style.width = targetWidth;
}

13.6 谨慎处理高频事件

滚动、拖动、鼠标移动和输入事件可能高频触发。

javascript 复制代码
let scheduled = false;

window.addEventListener(
  'scroll',
  () => {
    if (scheduled) {
      return;
    }

    scheduled = true;

    requestAnimationFrame(() => {
      updateStickyHeader();
      scheduled = false;
    });
  },
  { passive: true }
);

passive: true 适合不需要调用 preventDefault() 的触摸和滚动监听器。

防抖和节流也不能无条件使用。输入框搜索可以防抖,但点击反馈不能因为防抖而被延迟。


14. 渲染性能优化

浏览器渲染大致包括:

text 复制代码
JavaScript
   ↓
Style
   ↓
Layout
   ↓
Paint
   ↓
Composite

14.1 减少 DOM 规模

DOM 过大会增加:

  • HTML 解析时间;
  • 样式匹配时间;
  • 布局计算时间;
  • 内存占用;
  • 框架更新成本。

常见问题:

  • 无意义的多层包装元素;
  • 被隐藏但仍然完整渲染的大型弹窗;
  • 一次性渲染全部列表;
  • 重复保留已经不用的 DOM;
  • 组件层级映射成过深的真实 DOM。

14.2 限制布局影响范围

可以通过 CSS Containment 限制部分计算影响:

css 复制代码
.product-card {
  contain: layout paint;
}

或者:

css 复制代码
.virtual-list-item {
  contain: layout style paint;
}

使用 contain 可能改变定位、尺寸计算和绘制行为,需要逐个组件验证。

14.3 使用 requestAnimationFrame 更新视觉状态

javascript 复制代码
function updatePosition(element, x, y) {
  requestAnimationFrame(() => {
    element.style.transform = `translate(${x}px, ${y}px)`;
  });
}

requestAnimationFrame 会在下一帧绘制前执行回调,更适合视觉更新。

它不是通用的后台任务队列,也不会自动让复杂计算变快。

14.4 控制绘制区域

需要重点关注:

  • 大面积阴影;
  • 大范围模糊滤镜;
  • 半透明层叠;
  • 固定背景;
  • 大面积渐变动画;
  • 频繁变化的复杂 SVG;
  • 覆盖整个页面的动画图层。

这些效果不一定禁止使用,但应该通过 Performance 面板和低端设备测试其真实成本。


15. CLS 与页面稳定性优化

15.1 为媒体元素声明尺寸

html 复制代码
<img
  src="/images/article-cover.webp"
  width="1200"
  height="630"
  alt="文章封面"
>

视频和 iframe 也应该预留比例:

css 复制代码
.video-container {
  aspect-ratio: 16 / 9;
  background: #111;
}

.video-container iframe {
  width: 100%;
  height: 100%;
  border: 0;
}

15.2 为动态内容预留空间

css 复制代码
.ad-container {
  min-height: 250px;
  background: #f5f5f5;
}

即使广告最终没有返回,也要谨慎处理预留空间。

突然删除占位区域同样可能造成布局偏移。

15.3 骨架屏尺寸要接近真实内容

错误做法:

text 复制代码
骨架屏高度:200px
真实内容高度:700px

数据加载完成时,后续内容会整体下移。

正确做法是让骨架屏尽可能模拟:

  • 图片比例;
  • 标题行数;
  • 正文高度;
  • 按钮位置;
  • 卡片间距。

15.4 不要在页面顶部突然插入内容

例如页面加载完成后突然在顶部插入公告条,会把整个页面向下推。

可以:

  • 首次渲染时预留公告位置;
  • 将公告设计为覆盖层;
  • 在用户主动操作后插入;
  • 将非关键动态内容放在首屏以下。

15.5 动画优先使用 transform

不推荐:

css 复制代码
.notice {
  transition: top 200ms ease;
}

.notice.open {
  top: 0;
}

推荐:

css 复制代码
.notice {
  transform: translateY(-100%);
  transition: transform 200ms ease;
}

.notice.open {
  transform: translateY(0);
}

16. SPA、SSR、SSG 与水合性能

16.1 纯客户端渲染的性能问题

典型客户端渲染链路:

text 复制代码
下载 HTML 空壳
   ↓
下载 JavaScript
   ↓
解析和执行 JavaScript
   ↓
请求接口
   ↓
生成页面 DOM
   ↓
显示主要内容

如果 JavaScript 包很大,弱网和低端设备可能长时间白屏。

16.2 SSR

服务端渲染可以更早返回页面 HTML,使浏览器更早发现:

  • 文本内容;
  • CSS;
  • LCP 图片;
  • 页面链接。

但 SSR 并不一定自动更快。

SSR 仍然可能存在:

  • 服务端 TTFB 过高;
  • HTML 体积过大;
  • 客户端水合 JavaScript 过多;
  • 水合期间主线程阻塞;
  • 服务端和客户端内容不一致;
  • 页面看起来可用,但交互尚未完成。

16.3 SSG

静态站点生成在构建阶段产生 HTML,适合:

  • 博客;
  • 文档;
  • 新闻;
  • 产品介绍页;
  • 更新频率不高的公共页面。

它通常能够减少运行时服务端计算,但需要处理:

  • 内容更新;
  • 增量构建;
  • 缓存刷新;
  • 动态个性化;
  • 大量页面的构建时间。

16.4 部分水合与岛屿架构

不是所有页面内容都需要立即水合。

例如一篇文章中:

  • 标题和正文只需要静态 HTML;
  • 评论区可以进入视口后再加载;
  • 分享按钮可以在空闲阶段增强;
  • 在线编辑器可以在用户点击后加载。

理想目标是:

text 复制代码
默认发送可展示的 HTML
        +
只为真正需要交互的区域加载 JavaScript

16.5 避免重复请求数据

服务端已经获取并渲染的数据,不应该在客户端水合时无条件再次请求。

可以将必要的初始数据安全地序列化到页面中:

html 复制代码
<script type="application/json" id="initial-data">
{
  "productId": "P10001",
  "name": "示例商品"
}
</script>

读取时仍然应当进行类型和字段校验,不能因为数据来自服务端模板就完全信任。


17. 浏览器缓存与 CDN 优化

17.1 带内容哈希的静态资源

构建产物推荐使用内容哈希:

text 复制代码
app.8fd31a2c.js
style.170f20d1.css
logo.31a87ed2.svg

资源内容变化后,文件名也会变化,因此旧文件可以设置长期缓存:

http 复制代码
Cache-Control: public, max-age=31536000, immutable

17.2 HTML 缓存策略

HTML 文件通常不能像带哈希的静态资源一样缓存一年。

可以使用:

http 复制代码
Cache-Control: no-cache

no-cache 并不表示完全不存储,而是表示再次使用前需要向服务器验证。

如果页面包含高度敏感或用户专属内容,可以根据业务使用:

http 复制代码
Cache-Control: private, no-store

具体策略应根据内容是否公共、是否个性化以及是否允许中间缓存决定。

17.3 使用 ETag 或 Last-Modified

浏览器可以进行条件请求:

http 复制代码
If-None-Match: "asset-version-123"

资源未变化时,服务器返回:

http 复制代码
HTTP/1.1 304 Not Modified

这样可以避免重新传输完整响应体。

17.4 stale-while-revalidate

对于允许短时间使用旧内容的公共资源,可以考虑:

http 复制代码
Cache-Control: public, max-age=300, stale-while-revalidate=60

含义是:

  • 资源在 300 秒内保持新鲜;
  • 过期后的 60 秒内可以先返回旧内容;
  • 同时在后台重新验证。

是否适用取决于业务对数据一致性的要求。

余额、库存、支付状态等敏感数据不能机械套用该策略。

17.5 文本资源压缩

适合压缩:

  • HTML;
  • CSS;
  • JavaScript;
  • JSON;
  • SVG;
  • XML。

常见算法:

  • Brotli;
  • Gzip。

已经经过压缩的 JPEG、WebP、AVIF、MP4、ZIP 等文件,再进行 Gzip 通常收益很小。

17.6 CDN 优化

CDN 可以:

  • 缩短资源与用户之间的物理距离;
  • 缓存静态资源;
  • 减少源站压力;
  • 提高并发能力;
  • 降低跨区域网络抖动。

CDN 优化时需要检查:

  • 缓存命中率;
  • 回源比例;
  • 缓存键是否包含无意义参数;
  • Cookie 是否导致公共资源无法缓存;
  • Vary 是否造成缓存碎片;
  • 节点是否覆盖主要用户区域;
  • 缓存刷新机制是否可靠;
  • HTML 和静态资源是否使用了不同策略。

18. 第三方脚本优化

第三方脚本通常包括:

  • 数据统计;
  • 广告;
  • 在线客服;
  • 地图;
  • 支付 SDK;
  • A/B 测试;
  • 用户行为录制;
  • 社交分享;
  • 推荐系统。

第三方脚本可能带来:

  • DNS、TCP、TLS 连接;
  • JavaScript 下载;
  • 主线程解析和执行;
  • 长任务;
  • 隐私和安全风险;
  • 单点故障;
  • 页面布局偏移。

18.1 建立第三方脚本清单

脚本 负责人 是否首屏需要 体积 主线程耗时 故障影响
统计 SDK 数据团队 80 KB 60 ms
在线客服 客服团队 350 KB 220 ms
支付 SDK 支付团队 结算页需要 180 KB 100 ms

如果没有负责人,也无法说明业务价值,这个第三方脚本就应该被重新评估。

18.2 延迟非关键脚本

javascript 复制代码
async function loadCustomerService() {
  const module = await import('./customer-service.js');
  module.initialize();
}

document
  .querySelector('#customer-service-button')
  .addEventListener('click', loadCustomerService, { once: true });

在线客服不一定需要在用户打开首页的第一毫秒加载。

18.3 设置超时和降级

第三方接口不能无限阻塞页面核心流程。

核心内容和主按钮应尽量不依赖第三方统计、推荐或客服脚本是否成功。


19. 前端性能监控与 RUM

19.1 使用 web-vitals 采集指标

安装:

bash 复制代码
npm install web-vitals

采集:

javascript 复制代码
import {
  onCLS,
  onINP,
  onLCP
} from 'web-vitals/attribution';

function sendMetric(metric) {
  const payload = JSON.stringify({
    name: metric.name,
    value: metric.value,
    rating: metric.rating,
    id: metric.id,
    navigationType: metric.navigationType,
    route: getRouteTemplate(),
    release: window.APP_RELEASE
  });

  const sent = navigator.sendBeacon('/api/rum', payload);

  if (!sent) {
    fetch('/api/rum', {
      method: 'POST',
      body: payload,
      keepalive: true,
      headers: {
        'Content-Type': 'application/json'
      }
    }).catch(() => {
      // 性能上报失败不能影响用户正常使用
    });
  }
}

onLCP(sendMetric);
onINP(sendMetric);
onCLS(sendMetric);

19.2 不要直接上报完整 URL

以下 URL 可能包含敏感信息或造成高基数:

text 复制代码
/order/100001?token=secret
/user/987654/profile

建议转换成路由模板:

text 复制代码
/order/:orderId
/user/:userId/profile

同时不要上报:

  • 用户输入内容;
  • Access Token;
  • Cookie;
  • 身份证号;
  • 手机号;
  • 完整接口响应;
  • 未脱敏的用户 ID。

19.3 性能数据需要分组

建议按照以下维度分析:

  • 页面路由;
  • 应用版本;
  • 设备类型;
  • 操作系统;
  • 浏览器;
  • 网络类型;
  • 地理区域;
  • 新用户和老用户;
  • 登录和未登录;
  • 首次访问和站内跳转。

平均值可能掩盖长尾问题。

例如:

text 复制代码
平均 LCP:2.1 秒
第 75 百分位:3.8 秒
第 95 百分位:8.2 秒

平均值看起来正常,但大量用户的体验实际上很差。

19.4 将发布版本与性能数据关联

javascript 复制代码
window.APP_RELEASE = 'web-2026.08.27.1';

有了版本号,就可以判断:

  • 哪次发布后 LCP 变慢;
  • 某个 JavaScript 包升级是否导致 INP 回退;
  • 某个页面重构是否降低 CLS;
  • 回滚后指标是否恢复。

19.5 采样与数据量控制

高流量网站不一定要上报所有访问。

可以进行稳定采样:

javascript 复制代码
const shouldReport = Math.random() < 0.1;

if (shouldReport) {
  onLCP(sendMetric);
  onINP(sendMetric);
  onCLS(sendMetric);
}

采样比例应记录在分析系统中,并保证重要页面和异常场景仍有足够样本。


20. 使用 Lighthouse CI 建立性能门禁

性能优化不能只依赖人工检查。

可以使用 Lighthouse CI 在每次构建或合并请求中执行测试。

20.1 安装 Lighthouse CI

bash 复制代码
npm install --save-dev @lhci/cli

20.2 配置 lighthouserc.json

json 复制代码
{
  "ci": {
    "collect": {
      "startServerCommand": "npm run preview",
      "startServerReadyPattern": "Local",
      "url": [
        "http://localhost:4173/",
        "http://localhost:4173/products"
      ],
      "numberOfRuns": 3
    },
    "assert": {
      "assertions": {
        "categories:performance": [
          "warn",
          {
            "minScore": 0.9
          }
        ],
        "largest-contentful-paint": [
          "error",
          {
            "maxNumericValue": 2500
          }
        ],
        "total-blocking-time": [
          "error",
          {
            "maxNumericValue": 200
          }
        ],
        "cumulative-layout-shift": [
          "error",
          {
            "maxNumericValue": 0.1
          }
        ],
        "resource-summary:script:size": [
          "error",
          {
            "maxNumericValue": 307200
          }
        ]
      }
    }
  }
}

这里 resource-summary:script:size 使用字节作为单位,307200 约等于 300 KB。

20.3 配置执行命令

json 复制代码
{
  "scripts": {
    "build": "vite build",
    "preview": "vite preview",
    "perf": "npm run build && lhci autorun"
  }
}

运行:

bash 复制代码
npm run perf

20.4 为什么要运行多次

Lighthouse 会受到以下因素影响:

  • CI 机器负载;
  • CPU 调度;
  • 网络波动;
  • 浏览器后台任务;
  • 页面随机内容;
  • 接口响应变化。

建议:

  • 多次运行;
  • 固定测试环境;
  • 尽量使用稳定测试数据;
  • 对容易波动的分数设置警告;
  • 对资源体积、请求数量等确定性指标设置错误门禁。

性能门禁的目标不是让构建频繁误报,而是及时阻止明显的性能回退。


21. Web 性能优化实战案例

下面以一个电商首页为例。

以下数据仅用于展示优化过程,不代表所有项目都能获得相同结果。

21.1 优化前

指标 优化前
TTFB 1.2 秒
LCP 4.8 秒
INP 380 毫秒
CLS 0.24
首屏 JavaScript 820 KB
首屏图片 1.9 MB
请求数量 126
长任务数量 11

21.2 问题分析

通过 Network 和 Performance 面板发现:

  1. 首页接口串行调用三个下游服务;
  2. 首屏 Banner 使用 2600 像素宽的 PNG;
  3. Banner 地址需要执行 JavaScript 后才能获得;
  4. 首屏 Banner 被错误设置为 loading="lazy"
  5. 首页加载了后台编辑器和图表库;
  6. 在线客服 SDK 在页面初始化阶段执行;
  7. 商品列表一次渲染 500 个节点;
  8. 图片没有声明宽高;
  9. Web 字体加载了六个字重;
  10. 静态资源没有长期缓存。

21.3 第一阶段:优化后端和首屏链路

实施:

  • 并行调用互不依赖的服务;
  • 缓存公共推荐数据;
  • 删除一次不必要的重定向;
  • 在初始 HTML 中直接输出 Banner;
  • 为 Banner 设置 fetchpriority="high"
  • 移除 Banner 的懒加载。

结果:

text 复制代码
TTFB:1.2 秒 → 0.45 秒
LCP:4.8 秒 → 3.1 秒

21.4 第二阶段:优化图片

实施:

  • PNG 转换为 AVIF 和 WebP;
  • 使用 picture 提供回退;
  • 为移动端生成较小尺寸;
  • 使用 srcsetsizes
  • 为图片声明宽高;
  • 首屏外图片使用原生懒加载。

结果:

text 复制代码
首屏图片:1.9 MB → 620 KB
LCP:3.1 秒 → 2.3 秒
CLS:0.24 → 0.08

21.5 第三阶段:优化 JavaScript

实施:

  • 编辑器改为点击后动态导入;
  • 图表库只在数据分析页面加载;
  • 在线客服改为用户点击后加载;
  • 删除重复工具库;
  • 将首页脚本按页面拆分;
  • 商品列表改为分页和虚拟滚动。

结果:

text 复制代码
首屏 JavaScript:820 KB → 310 KB
长任务:11 个 → 3 个
INP:380 毫秒 → 165 毫秒

21.6 第四阶段:优化缓存和字体

实施:

  • 带哈希静态资源设置一年缓存;
  • HTML 使用协商缓存;
  • 字体从六个字重减少到两个;
  • 字体改为 WOFF2;
  • 只预加载首屏字体;
  • CDN 缓存键移除无意义查询参数。

结果:

指标 优化前 优化后
TTFB 1.2 秒 0.45 秒
LCP 4.8 秒 1.9 秒
INP 380 毫秒 150 毫秒
CLS 0.24 0.05
首屏 JavaScript 820 KB 310 KB
首屏图片 1.9 MB 620 KB
请求数量 126 61
长任务数量 11 2~3

21.7 案例中的关键经验

优化收益并不是来自某一个"性能技巧",而是来自完整链路:

text 复制代码
后端响应
  +
资源发现
  +
资源优先级
  +
资源体积
  +
JavaScript 执行
  +
DOM 渲染
  +
缓存策略

如果只压缩图片,而 LCP 图片仍然必须等待 JavaScript 执行后才被发现,收益可能非常有限。


22. 常见性能问题排查方法

现象 优先检查 常见解决方向
页面长时间白屏 TTFB、关键 CSS、主包 JS 服务端缓存、减少阻塞资源、SSR 或预渲染
LCP 图片很晚才请求 Network Initiator、HTML 源码 直接写入 HTML、preload、fetchpriority
图片下载完仍不显示 Performance 主线程、元素样式 拆分长任务、取消整体隐藏、减少客户端渲染依赖
点击按钮没有反馈 INP 分阶段、Long Task 尽早更新 UI、拆分任务、减少事件回调工作
输入框明显卡顿 输入处理、列表渲染 延迟非关键计算、虚拟列表、Web Worker
页面不断跳动 Layout Shift 轨迹 图片尺寸、广告占位、字体度量、骨架屏
首次访问慢 冷缓存 Network 减少首屏资源、压缩、CDN、关键资源优先级
二次访问仍然慢 Size、Cache-Control 长期缓存、内容哈希、协商缓存
手机慢但电脑正常 CPU 节流、真实设备 减少 JS、长任务和 DOM,延迟第三方脚本
某地区访问慢 RUM 地域数据、CDN 调整节点、减少跨区回源、检查 DNS
发布后突然变慢 版本维度、构建产物 对比资源体积、依赖版本和长任务
Lighthouse 波动较大 多次运行结果 固定环境、多次运行、关注确定性预算

排查时建议遵循:

text 复制代码
先确认问题
  ↓
找到具体指标
  ↓
定位指标的组成部分
  ↓
确定具体资源或任务
  ↓
一次只修改一个关键变量
  ↓
重新测量

23. Web 性能上线检查清单

23.1 HTML

  • 页面不存在无意义的重定向链;
  • 首屏关键内容存在于初始 HTML;
  • LCP 图片能够从 HTML 中直接发现;
  • 页面声明正确的字符编码;
  • 没有重复加载相同资源。

23.2 图片

  • 图片选择了合适格式;
  • 使用响应式图片;
  • 图片声明宽高;
  • 首屏外图片使用懒加载;
  • LCP 图片没有使用懒加载;
  • 只给少量关键图片设置高优先级;
  • 移动端没有下载明显过大的桌面图片。

23.3 CSS

  • 删除无用样式;
  • 没有全量引入不需要的组件样式;
  • 关键 CSS 体积可控;
  • 动画优先使用 transformopacity
  • 没有滥用 will-change
  • 长页面已评估延迟渲染方案。

23.4 JavaScript

  • 主包体积符合预算;
  • 页面按路由拆包;
  • 大型功能按需加载;
  • 没有重复依赖;
  • 没有明显长任务;
  • 大型计算已评估 Web Worker;
  • 第三方脚本不会阻塞核心内容;
  • 低端移动设备完成过测试。

23.5 字体

  • 使用 WOFF2;
  • 只加载实际使用的字体和字重;
  • 字体进行了合理子集化;
  • font-display 策略合理;
  • 只预加载关键字体;
  • 字体替换不会造成明显布局偏移。

23.6 缓存和 CDN

  • 静态资源文件名包含内容哈希;
  • 带哈希资源配置长期缓存;
  • HTML 使用适合业务的缓存策略;
  • 文本资源启用 Brotli 或 Gzip;
  • CDN 缓存命中率正常;
  • 不必要的 Cookie 和查询参数不会破坏缓存;
  • CDN 回源和缓存刷新经过验证。

23.7 性能监控

  • 已采集 LCP、INP 和 CLS;
  • 指标包含页面和发布版本;
  • 不采集敏感信息;
  • 使用第 75 百分位分析;
  • 能按设备、网络、浏览器和地域分组;
  • 发布后具有性能告警;
  • CI 中配置性能预算。

24. 常见问题

24.1 Lighthouse 100 分是否代表性能没有问题?

不代表。

Lighthouse 是实验室测试,结果受到测试环境、页面状态和工具规则影响。

真实用户可能使用:

  • 更慢的手机;
  • 更差的网络;
  • 不同浏览器;
  • 更复杂的数据;
  • 不同登录状态;
  • 更长的交互流程。

应该同时观察 Lighthouse、Chrome DevTools、资源预算和 RUM 数据。

24.2 请求越少越好吗?

不一定。

减少无意义请求通常有帮助,但不能为了减少请求,把所有代码合并成一个超大文件。

需要平衡:

  • 首屏资源数量;
  • 单个资源体积;
  • 缓存复用;
  • 按需加载;
  • 请求优先级;
  • HTTP/2 或 HTTP/3 多路复用。

24.3 使用 CDN 后为什么仍然很慢?

可能原因包括:

  • HTML TTFB 仍然很高;
  • CDN 缓存没有命中;
  • 缓存键配置错误;
  • 资源携带 Cookie;
  • 首屏 JavaScript 过大;
  • LCP 资源发现时间太晚;
  • 用户设备执行 JavaScript 很慢;
  • CDN 节点与主要用户区域不匹配。

CDN 主要优化网络分发,不能自动解决主线程和渲染问题。

24.4 图片已经很小,为什么 LCP 仍然很高?

可能是:

  • TTFB 高;
  • 图片请求开始得太晚;
  • 图片被懒加载;
  • 图片优先级低;
  • 图片需要等待 JavaScript 才能发现;
  • 主线程长任务阻塞绘制;
  • 元素被 CSS 隐藏;
  • 客户端渲染完成得太晚。

应该分析完整的 LCP 四阶段,而不是只看文件体积。

24.5 SSR 是否一定比 SPA 快?

不一定。

SSR 可以更早提供 HTML,但也可能存在:

  • 服务端响应慢;
  • 水合成本高;
  • JavaScript 体积仍然很大;
  • 页面可见但不可交互;
  • 服务端和客户端重复请求;
  • 水合不一致。

需要根据内容类型、用户访问路径和真实测量结果选择架构。

24.6 防抖是否能够解决所有 INP 问题?

不能。

防抖适合减少高频搜索、校验或接口请求,但会主动延迟回调执行。

如果按钮点击本来就应该立即反馈,使用防抖反而可能让体验更差。

INP 优化的核心是:

  • 减少输入延迟;
  • 缩短事件回调;
  • 减少布局和绘制成本;
  • 尽快产生下一次视觉反馈。

24.7 是否应该预加载所有重要资源?

不应该。

preload 会提升资源调度优先级。预加载过多资源会互相竞争,并挤占真正关键资源的带宽。

通常只预加载:

  • LCP 图片;
  • 首屏关键字体;
  • 首屏必须使用但发现较晚的资源。

任何预加载都应该通过 Network 瀑布图验证。


25. 总结

Web 性能优化不是一次性工作,而是一套持续治理流程。

完整的性能优化闭环应该是:

text 复制代码
建立指标
   ↓
采集真实用户数据
   ↓
识别性能最差的页面和用户群体
   ↓
使用实验室工具复现
   ↓
分析网络、主线程和渲染瓶颈
   ↓
实施小范围优化
   ↓
验证优化效果
   ↓
设置性能预算和 CI 门禁
   ↓
发布后持续监控

在实际项目中,可以优先处理以下问题:

  1. 降低 TTFB;
  2. 让 LCP 资源尽早被浏览器发现;
  3. 优化首屏图片格式、尺寸和优先级;
  4. 减少首屏 JavaScript;
  5. 拆分主线程长任务;
  6. 为图片、广告和异步内容预留空间;
  7. 延迟加载非关键功能和第三方脚本;
  8. 配置正确的浏览器缓存和 CDN;
  9. 建立真实用户性能监控;
  10. 使用性能预算防止版本回退。

性能优化最重要的原则是:

不凭感觉优化,不只看单次跑分,不把技巧当目标。先测量真实问题,再优化关键瓶颈,最后用持续监控证明效果。


参考资料

  1. Web Vitals
  2. 优化 Largest Contentful Paint
  3. 优化 Interaction to Next Paint
  4. 优化 Cumulative Layout Shift
  5. Chrome DevTools Performance 面板
  6. Chrome Performance Insights
  7. web-vitals 官方项目
  8. Lighthouse CI
  9. HTTP Caching RFC 9111
相关推荐
冬奇Lab1 小时前
一天一个开源项目(第200篇):next-forge - 生产级 Next.js SaaS 启动模板
前端·前端框架·开源
Profile排查笔记1 小时前
指纹浏览器怎么设置 IP?代理配置、检测与排错流程
前端·人工智能·后端·自动化
lzhdim1 小时前
13、JavaScript事件循环机制 - JavaScript学习系列文章
开发语言·前端·javascript·学习·ecmascript
liangshanbo12151 小时前
Vue 3 <script setup> 的本质原理是什么?它与普通 setup()有何区别?
前端·javascript·vue.js
IT_陈寒2 小时前
SpringBoot自动配置失效?你可能漏了这个小开关
前端·人工智能·后端
kyriewen2 小时前
我写了1个复盘Skill,和AI协作踩过的坑第二天自动变成护栏
前端·程序员·ai编程
风骏时光牛马2 小时前
AI提示词异常故障复盘分析
前端
SoaringHeart3 小时前
Flutter 进阶:NCanvasImageLoader 让 Canvas 也能画网络图
前端·flutter
计算机魔术师3 小时前
英伟达两个月叫停360亿美元生意:芯片巨头怕了反垄断?
前端