跳到主要内容

性能优化

你简历上「优化 TBT / FP / FCP / CLS / TTI / SEO」是真实经历,这页要确保被追问时能讲出机制。

:::tip 答这题的框架 不要一上来背清单。 先问 / 明确:优化哪个指标?

  • 首屏慢 → LCP → 关键资源加载路径
  • 点击没反应 → INP / TBT → 主线程长任务
  • 页面跳动 → CLS → 元素尺寸预留
  • 动画卡 → 帧率 → 合成层和重排

然后按 测量 → 定位瓶颈 → 针对性优化 → 验证 讲。有数据的优化才算优化。 :::

🔥 Core Web Vitals​

指标衡量好差
LCP(Largest Contentful Paint)最大内容元素渲染完成 —— 加载性能≤2.5s>4s
INP(Interaction to Next Paint)交互到下一帧渲染 —— 响应性(2024 年取代 FID)≤200ms>500ms
CLS(Cumulative Layout Shift)累积布局偏移 —— 视觉稳定性≤0.1>0.25

其他相关指标:

  • FCP(First Contentful Paint)—— 首次有内容绘制,衡量「白屏时间」
  • TTFB(Time to First Byte)—— 服务端响应速度,是 LCP 的前置
  • TBT(Total Blocking Time)—— 所有长任务(>50ms)超出 50ms 的部分之和。实验室环境下 INP 的代理指标
  • TTI(Time to Interactive)—— 可交互时间
  • FID —— 已被 INP 取代(FID 只测「第一次交互的延迟」,INP 测所有交互的整体表现,更真实)

各指标的优化手段​

LCP —— 它的构成是 TTFB + 资源加载时间 + 渲染时间:

  1. 找出 LCP 元素是什么(通常是首屏大图或标题块)—— DevTools Performance 面板直接标注
  2. 如果是图片:<link rel="preload" as="image"> 提前加载、用 fetchpriority="high"、首屏图片绝对不要 loading="lazy"(这是常见的反向优化)、用 WebP/AVIF、响应式图片(srcset)
  3. 减少 TTFB:CDN、服务端缓存、SSR/SSG
  4. 消除渲染阻塞资源:关键 CSS 内联、非关键 CSS 异步加载、JS 用 defer/async
  5. 避免 LCP 元素依赖 JS 才能显示(纯 CSR 的 LCP 必然很差)

INP / TBT —— 核心是减少主线程长任务:

  1. 代码分割 —— 路由级和组件级懒加载,减少首屏 JS 体积
  2. 长任务切片 —— 把大计算拆成小块,用 scheduler.yield()(新 API)或 MessageChannel 让出主线程
  3. Web Worker —— 把纯计算移出主线程
  4. 减少不必要的重渲染 —— React 的 memo 化、虚拟列表
  5. 防抖节流 —— 高频事件(输入、滚动、resize)
  6. passive: true —— 滚动相关事件

CLS —— 布局偏移的四个主要来源:

  1. 图片 / 视频没有尺寸 → 给 width/height 属性或 aspect-ratio(这样浏览器能提前预留空间)
  2. 动态插入的内容(广告、通知条)→ 预留固定高度的容器
  3. Web Font 导致的文字重排(FOUT) → font-display: swap + size-adjust 或 preload 字体
  4. 骨架屏尺寸和真实内容不一致 → 对齐尺寸

注意:用户交互 500ms 内触发的布局变化不计入 CLS(因为那是预期的)。

资源加载优化​

资源提示(Resource Hints):

标签作用用在
dns-prefetch提前做 DNS 解析第三方域名
preconnect提前建立连接(DNS + TCP + TLS)确定会用到的关键第三方域(字体、API)
preload提前加载当前页需要的资源,不执行关键字体、首屏图片、关键 CSS
prefetch空闲时加载下一个页面可能用的资源,低优先级预测的下一步导航
modulepreload预加载 ES 模块(含依赖)关键模块

preload 用错会变负优化 —— 它是高优先级,会和真正的关键资源抢带宽。只对「确定首屏需要、但发现得晚」的资源用(比如 CSS 里 @font-face 引用的字体,浏览器要等 CSS 解析完才发现它)。

图片优化(通常是页面体积的大头):

<img src="hero.jpg"
srcset="hero-400.webp 400w, hero-800.webp 800w, hero-1600.webp 1600w"
sizes="(max-width: 600px) 100vw, 50vw"
width="1600" height="900"
fetchpriority="high"
alt="...">
  • 格式:AVIF(最小)> WebP(兼容性好)> JPEG。用 <picture> 做降级
  • 响应式:srcset + sizes 让浏览器按屏幕和 DPR 选最合适的
  • 懒加载:loading="lazy",但首屏图片不要加
  • 必须写 width/height —— 防 CLS
  • CDN 实时裁剪 —— 按需生成尺寸,不要前端拿原图缩放

字体优化:font-display: swap(先用后备字体显示,字体到了再换,避免不可见文字)、子集化(中文字体必做,全量字体几 MB)、preload 关键字体、优先用系统字体栈。

渲染层面​

虚拟列表 —— 长列表只渲染可见区域的若干项。这是「渲染一万条数据」的标准答案。要点:容器固定高度 + 内部撑开总高度的占位元素 + 根据 scrollTop 算可见范围 + 上下各多渲染几个缓冲项。不定高的情况要动态测量并缓存高度。

合成层(GPU 加速) —— 用 transform/opacity 做动画,它们跳过布局和绘制,直接在合成线程执行,不占主线程。will-change: transform 可以提前提升为独立层,但滥用会导致内存暴涨(每层都要单独的位图),用完要移除。

content-visibility: auto —— 告诉浏览器「这个元素不在视口内时跳过渲染」,对长页面效果显著。配合 contain-intrinsic-size 避免滚动条跳动。

减少重排 —— 见浏览器原理的强制同步布局。

构建与传输​

代码分割:

// 路由级(最重要)
const Page = lazy(() => import('./Page'))

// 组件级:弹窗、图表、编辑器这类非首屏必需的
const Chart = lazy(() => import('./Chart'))

// 条件加载:只在需要时才下载 polyfill
if (!window.IntersectionObserver) await import('intersection-observer')

Tree Shaking 生效的前提(高频追问):

  1. 必须是 ESM(import/export),CommonJS 的动态性让静态分析无法进行
  2. 没有副作用 —— 在 package.json 里声明 "sideEffects": false,或列出有副作用的文件
  3. 不要用 import * as X 后整体传递

常见失效原因:引入了整个库(import _ from 'lodash' 而不是 lodash-es 的按需导入)、Babel 把 ESM 转成了 CJS(要设 modules: false)、类的方法无法被 shake(因为可能被动态访问)。

压缩与传输 —— Brotli 比 gzip 小 15~25%,现代浏览器都支持;静态资源用 CDN;HTTP/2 下不要合并文件和域名分片(见网络与缓存)。

缓存策略 —— 带哈希的静态资源永久缓存 + HTML 不缓存。见网络与缓存。

测量工具​

工具用途
Lighthouse实验室数据,给评分和具体建议。注意是模拟环境,和真实用户体验有差距
DevTools Performance火焰图,定位长任务、重排、具体函数耗时。最重要的工具
DevTools Coverage找未使用的 CSS/JS 代码
web-vitals 库采集真实用户(RUM)的 CWV 数据上报
CrUX / PageSpeed InsightsChrome 真实用户数据(有的话比 Lighthouse 可信)
PerformanceObserver自己采集各类性能条目
Bundle Analyzer可视化产物体积构成,找出意外引入的大包

:::warning 实验室数据 ≠ 真实数据 Lighthouse 跑 95 分不代表用户体验好 —— 它用的是固定的模拟网速和设备。真实用户数据(RUM)才是最终标准,尤其要看 p75 分位而不是平均值(CWV 的官方标准就是 p75)。

面试主动区分这两者,比只会报 Lighthouse 分数专业得多。 :::

怎么讲自己的优化经历​

你简历上有「优化 X4 产品页的 TBT/FCP/CLS」,被追问时按这个结构答:

  1. 当时的问题和数据 —— 「LCP 4.2s,首屏是一张 2MB 的 PNG,且被 GSAP 的初始化阻塞」
  2. 怎么定位的 —— 「Performance 面板发现主线程有一个 800ms 的长任务,是动画库在解析配置」
  3. 做了什么,为什么这么选 —— 「图片换 WebP + 响应式 srcset(体积降 70%);动画初始化推迟到 requestIdleCallback;首屏图片加 preload 和 fetchpriority=high」
  4. 结果 —— 「LCP 从 4.2s 降到 1.8s,TBT 从 800ms 降到 120ms」
  5. 代价和权衡 —— 「WebP 需要对老 Safari 做 <picture> 降级;动画推迟导致首次进入有轻微延迟感,所以加了 CSS 的静态初始态避免闪烁」

第 5 点最加分 —— 能主动说出代价,说明你真的做过而不是背的。