← 全部材料 性能
PERFORMANCE · 14 个高频点

性能优化:先量,再改

性能这块最容易背成一份清单:"用 CDN、压缩图片、懒加载、虚拟列表"。但面试官真正想看的是——你为什么认为这里是瓶颈。所以下面每一节都先给机制,再给手段:知道它卡在哪一步,才知道该动什么。

01机制先于手段每条优化都对应一个渲染阶段或网络环节,答不上机制就只是背清单。
02会看数据能说清用 Lighthouse 还是 Performance 面板、看 p75 还是平均值,比记住分数有用。
03动手试第 6 节有个可点的演示:同步阻塞时页面心跳会停,分片执行时不会。差别一眼就能看出来。
1

三个核心指标:LCP、INP、CLS★

FID 已经被换掉了,别再答它
Core Web Vitals 现在是三个:LCP 看加载(最大内容什么时候画出来)、INP 看交互(点击到下一帧之间的延迟)、CLS 看稳定(布局有没有跳)。FID 在 2024 年被 INP 取代。

三个指标各管什么

  • LCP:视口内最大的图片或文本块出现的时间。好 < 2.5s
  • INP:所有交互的延迟取一个高分位值。好 < 200ms
  • CLS:布局偏移分数。好 < 0.1

FID 为什么被淘汰

  • FID 只量了"输入延迟",不量处理时间
  • 它不知道点击之后页面多久才更新
  • INP 覆盖了完整的交互生命周期
面试怎么答核心指标是 LCP 衡量加载、INP 衡量交互响应、CLS 衡量视觉稳定。FID 因为只测输入延迟、不覆盖处理过程,已被 INP 取代。三个指标都要看现场数据,不能只看本地跑分。
2

怎么量:实验室数据 vs 现场数据★

两者结论经常不一样
实验室(Lighthouse、Performance 面板)可复现、能定位问题,但用的是模拟网络和你的设备;现场(RUM)是真实用户,但只能告诉你"有问题",说不清为什么。两者要配合用。
现场数据只能靠 PerformanceObserver 收集// LCP new PerformanceObserver((list) => { const entries = list.getEntries(); const last = entries[entries.length - 1]; report('LCP', last.startTime, last.element); }).observe({ type: 'largest-contentful-paint', buffered: true }); // CLS:累加,注意只在页面隐藏时上报最终值 let cls = 0; new PerformanceObserver((list) => { for (const e of list.getEntries()) { if (!e.hadRecentInput) cls += e.value; } }).observe({ type: 'layout-shift', buffered: true }); // INP:记录每次交互的时长,取高百分位 new PerformanceObserver((list) => { for (const e of list.getEntries()) report('interaction', e.duration); }).observe({ type: 'event', buffered: true, durationThreshold: 40 });
面试怎么答实验室数据用 Lighthouse 和 Performance 面板,可复现能定位,但环境和设备是模拟的。现场数据用 RUM 加 PerformanceObserver 收集真实用户的 LCP、INP、CLS,要注意在 visibilitychange 时用 sendBeacon 上报最终值。两者结合才能既知道有没有问题,又知道问题在哪。
3

关键渲染路径:什么在阻塞首屏★

CSS 阻塞渲染,JS 阻塞解析
首屏慢,先看有没有阻塞资源。CSS 没下完就不会渲染(避免样式闪烁),JS 没执行完就不会继续解析 DOM(因为脚本可能改 DOM)。两句话解释了 90% 的首屏优化。
面试怎么答CSS 阻塞渲染是因为浏览器要避免无样式闪烁,JS 阻塞 DOM 解析是因为脚本可能修改 DOM。优化手段是给脚本加 defer 或 async、把关键 CSS 内联、非关键样式异步加载、字体用 font-display: swap。
4

五个资源提示,别用混

preload 用错等于多下一次
preload 是"这个页面马上要用,优先下";prefetch 是"下个页面可能要用,闲时下";preconnect 只建连接不下载;dns-prefetch 只做 DNS。用错会出现"下载了但没用上"的警告。

常见误用

  • preload 了但当前页面没用 → 控制台警告,白占带宽
  • 漏写 as → 类型不明,可能重复下载
  • preload 字体忘了 crossorigin → 下两次
  • prefetch 太多 → 抢当前页面的带宽

正确姿势

  • 只 preload 首屏必备的那 1–3 个资源
  • preload 必须和实际请求的 as、crossorigin 一致
  • preconnect 控制在 4 个以内(每个都要握手)
  • prefetch 只用于明确的下一个页面
面试怎么答preload 用于当前页面的关键资源,必须正确声明 as 和 crossorigin,否则会重复下载。prefetch 是空闲时预取下一个页面的资源。preconnect 提前完成 DNS 加 TCP 加 TLS,适合第三方域名,但数量要控制。dns-prefetch 只做域名解析,开销最小。
5

图片优化:格式、响应式与懒加载的矛盾★

懒加载别把 LCP 图也懒掉
图片优化的顺序是:先选对格式(AVIF/WebP)→ 再对尺寸(srcset + sizes)→ 最后才懒加载。而且首屏那张图绝对不能懒加载,否则 LCP 会明显变差。
面试怎么答图片先选格式再做响应式,用 picture 做 AVIF/WebP 降级,用 srcset 和 sizes 按视口选尺寸。必须写 width 和 height 防止 CLS。首屏图不能加 loading=lazy,应该用 fetchpriority=high 提升优先级。
6

长任务与时间切片★

50ms 是那条线
主线程上一个超过 50ms 的任务就会让交互变迟钝——因为浏览器要等它跑完才能响应输入。解决办法不是"跑得更快",而是拆成小片,中间让出主线程。
亲眼看到主线程被独占
页面心跳(每个动画帧 +1)
0
点上面的按钮,盯着数字看它停不停。
面试怎么答长任务指占用主线程超过 50ms 的任务,会让输入和渲染排队。优化方式是把任务切片,每 5ms 左右让出主线程,用 setTimeout 或 scheduler.yield,让浏览器有机会处理输入和渲染。计算密集型任务应该放到 Web Worker。
7

布局抖动:读写分离★

为什么一个循环能卡到掉帧
浏览器为了性能会攒着样式改动、批量计算。但只要你读一个依赖布局的属性,它就必须立刻把攒的改动全算完。循环里读一次写一次,就变成每轮一次强制同步布局。

触发强制布局的读操作

  • offsetTop / offsetLeft / offsetWidth / offsetHeight
  • scrollTop / scrollHeight
  • clientWidth / clientHeight
  • getComputedStyle()
  • getBoundingClientRect()

只写不读就不会抖

  • 把读集中在前一轮
  • 把写集中到后一轮
  • 需要动画就用 transform
  • 用 requestAnimationFrame 包住写操作
面试怎么答布局抖动的根因是读写交替:读布局属性会强制浏览器立刻完成之前所有待处理的样式计算。解决办法是把读操作和写操作分批,先全部读出来再统一写,或者用 requestAnimationFrame 把读写分到不同帧。
8

动画:只动 transform 和 opacity★

这两个属性可以完全绕开布局和绘制
动画卡不卡,取决于它触发了哪一步。改 width 要走布局和绘制,改 transform 只需要合成——在 GPU 上挪一下图层,几乎不占主线程。
改 width/topLayout → Paint → Composite
→
改 colorPaint → Composite
→
改 transformComposite
→
改 opacityComposite
面试怎么答动画性能取决于触发的渲染阶段。改几何属性触发重排最贵,改颜色触发重绘,改 transform 和 opacity 只触发合成,可以跑在 GPU 上。所以动画优先用这两个属性,用 requestAnimationFrame 驱动,will-change 要谨慎使用并及时移除。
9

虚拟列表:只渲染看得见的那几行

一万条数据不该产生一万个 DOM
虚拟列表的核心就三步:只渲染可视区 + 上下缓冲、用总高度撑出滚动条、用 transform 偏移到正确位置。定高和不定高是两种实现难度。
最小实现的关键计算// 已知每行高 40,容器高 600,滚动到 scrollTop const start = Math.floor(scrollTop / ROW_H) - BUFFER; // 可视区第一行 const visibleCount = Math.ceil(VIEW_H / ROW_H) + BUFFER * 2; const end = start + visibleCount; // 只渲染 [start, end) 这一段 // 外层容器高度 = total * ROW_H,撑出滚动条 // 内层偏移 = start * ROW_H,用 transform 而不是 top
面试怎么答虚拟列表只渲染可视区和缓冲区内的元素,用总高度撑出滚动条,用 transform 偏移内容位置。关键是避免改 top 触发重排。不定高的场景需要动态测量并缓存位置,实现复杂度会明显上升。
10

缓存分层:从内存到 CDN

同一个请求可能被五层挡下来
浏览器请求一个资源,会依次经过内存缓存 → Service Worker → 磁盘 HTTP 缓存 → CDN → 源站。优化缓存不是"加个 Cache-Control",而是想清楚每一层该放什么。
内存缓存当前页面会话,关掉就没
→
Service Worker你写的策略,可离线
→
HTTP 缓存max-age / ETag
→
CDN边缘节点,按地域命中
→
源站最后一道
面试怎么答缓存是分层的:内存缓存、Service Worker、HTTP 缓存、CDN、源站依次兜底。静态资源用内容 hash 配长 max-age 走 HTTP 缓存和 CDN,HTML 用 no-cache 保证入口及时更新,需要离线能力时再加 Service Worker。
11

打包层面:能删的删,能拆的拆

性能优化里性价比最高的一段
打包优化只有两件事:让代码变小(tree-shaking、按需引入、替代大依赖)和让首屏少加载(代码分割、路由懒加载)。前者省带宽,后者省时间。

让体积变小

  • 按需引入(import { x } from lodash-es)
  • 用轻量替代(dayjs 替 moment)
  • 检查有没有整包引入图标库
  • production 构建 + 压缩 + brotli

让首屏变少

  • 路由懒加载
  • 把不关键的第三方脚本延后加载
  • 抽出公共依赖,利用缓存
  • 分析包体,找最大的那几块
面试怎么答打包优化分两条线:减小体积靠 tree-shaking、按需引入、替换大依赖和 brotli 压缩;减少首屏加载靠路由懒加载和代码分割。动手前先用包体分析工具看最大的那几块,通常收益比逐项优化大得多。
12

前端内存泄漏的四种样子★

SPA 里特别容易中招
单页应用不刷新页面,所以泄漏会一直累积。四种典型:忘清定时器、忘解绑监听、闭包留住大对象、缓存无限增长。
面试怎么答前端泄漏主要是忘清定时器和事件监听、闭包持有大对象、缓存无上限增长、异步回调持有已卸载组件的引用。排查用堆快照做前后对比看增长,修复靠把所有启动动作和清理动作配对,缓存加容量上限或改用 WeakMap。
13

性能预算与线上监控

没有预算的优化永远做不完
性能不是"越优化越好",而是定一个能接受的上限,然后守住它。预算要写进 CI,监控要看分位数而不是平均值。
预算写成可执行的规则// 体积预算:超过就构建失败 budgets: [ { type: 'initial', maximumWarning: '200kb', maximumError: '300kb' }, { type: 'anyComponentStyle', maximumWarning: '6kb' }, ] // 指标预算:写在 CI 里比对 LCP p75 <= 2500ms INP p75 <= 200ms CLS p75 <= 0.1
面试怎么答性能预算就是把体积和指标的阈值写进 CI,超标直接失败。监控要看 p75 分位而不是平均值,按低端设备和弱网分层分析。每次合入都做回归比对,避免性能随时间悄悄退化。
14

速答清单

考前十分钟只看这页
01三个核心指标是什么?
LCP 加载、INP 交互、CLS 稳定。FID 已被 INP 取代,因为它不覆盖处理时间。
02CSS 和 JS 分别阻塞什么?
CSS 阻塞渲染(怕样式闪烁),JS 阻塞 DOM 解析(因为可能改 DOM)。
03defer 和 async 的区别?
defer 保序且在 DOMContentLoaded 前执行;async 谁先下完谁跑,不保序。有依赖用 defer。
04preload 和 prefetch?
preload 是当前页关键资源,必须写对 as 和 crossorigin;prefetch 是空闲时预取下一个页面。
05图片怎么优化?
AVIF/WebP 加 picture 降级、srcset 响应式、必须写宽高防 CLS、首屏图不能 lazy。
06什么是长任务?
占用主线程超过 50ms。切片处理,每 5ms 左右让出主线程。
07布局抖动怎么解决?
读写分离。读 offsetWidth 等属性会强制同步布局,把读和写分批就好。
08为什么动画只用 transform?
只触发合成,不经过布局和绘制,可以跑在 GPU 上。
09虚拟列表的原理?
只渲染可视区和缓冲,用总高度撑滚动条,用 transform 偏移内容。
10性能预算怎么做?
体积和指标阈值写进 CI,超标就失败;监控看 p75 不看平均值,按设备分层。