前端面试知识库
工程化与场景题

性能优化

从指标定义、瓶颈定位到方案落地,构建性能面试的完整回答路径。

性能优化题最怕一上来就罗列手段。更好的回答顺序:先讲你关注的指标,再定位瓶颈在网络、渲染还是脚本执行,最后说具体方案和验证方式。

高频主线

  • Core Web Vitals 三大指标的含义、阈值与优化方向
  • 关键渲染路径:HTML/CSS/JS 如何变成屏幕像素
  • 资源加载策略:如何缩短首屏时间
  • 重排 / 重绘 / 合成:触发条件与代价排序
  • 构建优化:如何减少打包体积和运行时开销
  • 框架级优化:Vue / React 渲染模型如何影响 CWV
  • 水合与 SSR:隐藏成本与应对策略
  • 性能 API:如何测量和监控真实用户体验

Core Web Vitals — 用户体验的三个维度

指标全称衡量维度良好需改进较差
LCPLargest Contentful Paint加载 — 最大内容渲染时间≤ 2.5s2.5s~4s> 4s
INPInteraction to Next Paint交互 — 用户操作到下一帧绘制延迟≤ 200ms200ms~500ms> 500ms
CLSCumulative Layout Shift视觉稳定性 — 意外布局偏移总分≤ 0.10.1~0.25> 0.25

INP 于 2024 年 5 月正式取代 FID 成为核心指标。FID 只测首次交互延迟,INP 测量所有交互的完整过程(输入延迟 + 事件处理 + 绘制延迟)。FID 是 TTI 对用户影响的"真实度量"——如果用户在 TTI 时刻未交互,TTI 对用户无实际影响。

辅助指标 — 补量中间过程

指标含义良好阈值数据来源功能角色
TTFB首字节时间(DNS + TCP + TLS + 服务器处理)≤ 800ms字段 + 实验室衡量服务端响应速度
FCP首次内容绘制≤ 1.8s字段 + 实验室LCP 的前置指标
TBT总阻塞时间(FCP~TTI 间长任务超出 50ms 部分之和)≤ 200ms实验室INP 的实验室预测指标
TTI可交互时间≤ 5s实验室需 5s 安静窗口计算
FMP首次有意义绘制Lighthouse 6.0 已弃用,被 LCP 替代

TTI 计算规则:FCP 之后,找到至少 5 秒的安静窗口(无长任务 + 不超过 2 个进行中的 GET 请求),再往前回溯到最后一个长任务结束时间。TBT 是 FID/INP 的实验室预测指标——没有真实用户交互数据时,TBT 可预测交互延迟。

关键渲染路径 — 从字节到像素

浏览器将 HTML、CSS、JavaScript 转换为屏幕像素的完整流程:

HTML 字节 → Token → Node → DOM 树
CSS 字节 → Token → Node → CSSOM 树
DOM + CSSOM → 渲染树 → 布局 → 绘制 → 合成

阻塞关系 — 谁挡住了渲染

资源阻塞行为优化方式
CSS阻塞渲染(CSSOM 完成前不显示内容,避免 FOUC)内联关键 CSS、非关键 CSS 异步加载
同步 JS阻塞 DOM 解析defer / async
async JS不阻塞解析,下载完立即执行(顺序不保证)独立第三方脚本
defer JS不阻塞解析,DOM 完成后按序执行业务脚本

CSS 阻塞 JS 执行:浏览器必须等 CSSOM 完成后才执行 JS,因为 JS 可能读取样式信息。加载顺序:CSS → JS → 渲染。

优化三原则

  1. 减少关键资源数量:内联关键 CSS、defer 非关键 JS
  2. 减少关键资源体积:压缩、Tree-shaking、代码分割
  3. 缩短关键路径长度preload 预加载关键资源、减少依赖链深度
<link rel="preload" href="critical-font.woff2" as="font" crossorigin>
<link rel="preconnect" href="https://cdn.example.com">
<link rel="dns-prefetch" href="https://analytics.example.com">

资源加载优化 — 缩短首屏时间

资源提示 — 控制加载优先级

提示作用优先级适用场景
dns-prefetchDNS 预解析第三方域名
preconnectDNS + TCP + TLS 预建立连接CDN、API 服务器
preload预加载当前页面必需资源关键字体、英雄图
prefetch预获取未来可能需要的资源下一页路由资源
modulepreload预加载 ES Module关键模块

preload 优先级高,加载当前页面必需但出现较晚的资源;prefetch 优先级低,加载后续路由可能需要的资源。两者不是随便用,而是有明确的优先级和场景区分。

图片优化 — 最大的体积优化空间

策略做法收益
现代格式WebP / AVIF(比 JPEG 小 50%+)减少传输体积
响应式图片<picture> + srcset + sizes按设备加载合适尺寸
懒加载loading="lazy" + Intersection Observer缩短关键路径
尺寸声明<img> 设置 width + heightaspect-ratio避免 CLS
缩略图策略大图先用缩略图,hover 时才加载全图按交互意图按需加载
图片压缩image-webpack-loader / TinyPNG减少文件体积

缩略图策略不同于懒加载(基于可视区域),它是基于用户交互意图——如果用户从未 hover,就节省了下载大图的时间。

代码分割与懒加载 — 按需加载

// 路由级懒加载
const routes = [
  { path: '/dashboard', component: () => import('./Dashboard.vue') }
]

// React 组件懒加载
const HeavyChart = React.lazy(() => import('./HeavyChart'))

// 动态导入 — 体积大但不一定用到的模块
import('./toastify').then((module) => {
  module.toast('Hello World')
})
<!-- 关键 CSS 内联,非关键 CSS 异步加载 -->
<style>/* 关键 CSS 内联在 head */</style>
<link rel="preload" href="non-critical.css" as="style"
      onload="this.rel='stylesheet'">

<!-- 或使用 media 属性延迟 -->
<link rel="stylesheet" href="print.css" media="print">
/* font-display: swap 避免 FOIT(不可见文本闪烁) */
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter.woff2') format('woff2');
  font-display: swap;
}

渲染优化 — 减少像素计算开销

重排 / 重绘 / 合成 — 代价递减的三层

类型触发条件代价优化方式
重排(Layout)改变几何属性(宽高、位置、边距)批量读写分离、避免逐条改样式
重绘(Paint)改变外观属性(颜色、阴影、背景)合理分层、减少绘制区域
合成(Composite)transform / opacity 动画GPU 处理,不触发重排重绘

动画只用 transformopacity——它们触发合成而非重排/重绘,是性能最优的动画属性。避免用 top/left/width/height 做动画。

减少重排的操作策略

做法原因
class 切换替代逐条改样式单次重排而非多次
复杂操作前 display:none 隐藏隐藏元素操作不触发重排
DocumentFragment 批量插入一次性插入,减少重排次数
先读后写,读写分离避免强制同步布局(Layout Thrashing)
contain CSS 属性限制布局和绘制范围,隔离子树

合成层与 GPU 加速 — 跳过重排重绘

触发合成层的条件:

  • 3D transformtranslate3dtranslateZ
  • will-change 属性
  • position: fixed
  • <video><canvas>、CSS opacity 动画

will-change 只在即将变化时设置,变化完成后移除。过度使用导致内存消耗过大,每个合成层都需要独立内存。

高频事件优化 — 控制调用频率

方式特点适用场景
防抖(debounce)一段时间后只执行一次,多次变最后一次搜索输入、resize
节流(throttle)固定频率执行,多次变规定时间内一次scroll、mousemove
事件委托父元素统一监听,减少注册数量列表项点击

虚拟滚动 — 大列表的必选项

大量数据列表只渲染视口内元素,减少 DOM 节点数。Vue 可用 vue-virtual-scroller,React 可用 react-window(比 react-virtualized 更轻量,同一作者)。

框架级性能优化 — Vue / React 如何影响 CWV

框架的渲染模型直接决定了 Core Web Vitals 的表现。理解框架如何影响指标,才能在架构层面做出正确选择。

框架渲染模型对 CWV 的影响

指标Vue 3React影响机制
LCP编译优化减少 JS 体积 → 更快的首次渲染运行时 Reconcile → 需更多 JS 执行时间框架 JS 体积和执行时间直接影响 LCP
INP响应式精确更新 → 最小化重渲染范围setState 触发子树协调 → 可能过度渲染更新粒度越细,交互延迟越低
CLSv-memo / shallowRef 减少不必要更新useMemo / React.memo 减少重渲染无效更新导致 DOM 抖动 → CLS 升高

LCP — 框架层面的优化

// 路由懒加载 — 减少首屏 JS 体积
const routes = [
  { path: '/dashboard', component: () => import('./Dashboard.vue') }
]

// 异步组件 — 按需加载重型组件
const HeavyChart = defineAsyncComponent(
  () => import('./HeavyChart.vue')
)

// shallowRef — 大型数据避免深层代理开销
const bigList = shallowRef<Item[]>([])

// v-once — 纯静态内容跳过更新
// <span v-once>{{ initialValue }}</span>
// React.lazy + Suspense — 代码分割
const HeavyChart = React.lazy(() => import('./HeavyChart'))

function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <HeavyChart />
    </Suspense>
  )
}

// useMemo — 缓存计算结果,减少重渲染开销
const processed = useMemo(
  () => heavyCalc(data), [data]
)

// React.memo — 跳过 props 未变的重渲染
const Item = React.memo(({ value }) => <li>{value}</li>)

LCP 的核心瓶颈是"最大的可见内容"何时出现。框架层面能做的是:减少首屏 JS 体积(代码分割)、减少 JS 执行时间(编译优化 / 缓存计算)、避免阻塞渲染的同步脚本。

INP — 减少交互延迟

// v-memo — 依赖不变则跳过更新,减少重渲染
// <div v-for="item in list" :key="item.id"
//      v-memo="[item.selected]">
//   <ExpensiveComponent :item="item" />
// </div>

// shallowRef — 只追踪 .value,不深层代理
const data = shallowRef({ items: [] })
// 嵌套修改不触发更新,需整体替换
data.value = { items: newData }

// effectScope — 批量管理副作用,一次性清理
const scope = effectScope()
scope.run(() => {
  watch(source, callback)
  // ...更多 watch/computed
})
scope.stop() // 一次性释放所有
// useTransition — 标记低优先级更新,不阻塞用户输入
const [isPending, startTransition] = useTransition()

function handleSearch(query) {
  startTransition(() => {
    setSearchResults(heavyFilter(query))
  })
  // 输入框立即响应,搜索结果延迟更新
}

// useDeferredValue — 延迟渲染某个值
const deferredQuery = useDeferredValue(query)
// 搜索列表用 deferredQuery,输入框用 query

// flushSync — 强制同步更新(慎用)
flushSync(() => setCount(c => c + 1))
// 之后的代码能访问更新后的 DOM

INP 的核心是"交互到绘制"的延迟。Vue 通过编译期标记(PatchFlag + Block Tree)精确更新,React 通过优先级调度(Lane + useTransition)让紧急更新优先。两种思路不同,目标一致:减少用户感知到的卡顿。

CLS — 避免布局抖动

策略Vue 3React
图片占位<img>width/heightaspect-ratio同左
条件渲染占位v-if 切换时用骨架屏占位条件渲染用骨架屏占位
避免动态注入不在运行时动态插入未预留空间的元素同左
CSS containmentcontain: layout style paint同左(框架无关)
列表 key 稳定v-for 用稳定 key 避免重排map 用稳定 key 避免重排

CLS 的根因是"渲染后布局发生意外变化"。框架层面的影响主要来自:异步数据加载后 DOM 突然出现、条件渲染切换导致元素位移。解法是预留空间(骨架屏 + 固定尺寸)和稳定 key。

水合(Hydration)— SSR 的隐藏成本

SSR 虽然改善了 LCP,但引入了水合开销:客户端需要重新执行 JS 恢复交互能力。水合期间会阻塞 INP。

SSR 流程:服务端渲染 HTML → 传输 → 客户端显示(可读但不可交互)→ 水合(JS 执行绑定事件)→ 可交互
                                  ↑ LCP 在此           ↑ TTI 在此           ↑ INP 受水合影响
策略原理适用场景
选择性水合只水合可见区域的交互组件大页面 + 部分交互
Islands Architecture页面由独立交互"岛屿"组成,非岛屿纯静态内容站 + 少量交互
流式 SSRHTML 分块传输,先到先渲染长页面 + 慢数据
Resumability不重新执行 JS,直接恢复状态(Qwik)极致首屏性能
// Vue 3.3+ 流式 SSR
import { renderToWebStream } from 'vue/server-renderer'

// Nuxt 3 默认启用流式渲染
// 组件级 Suspense 边界实现分段传输
// React 18 Streaming SSR
import { renderToPipeableStream } from 'react-dom/server'

renderToPipeableStream(<App />, {
  bootstrapScripts: ['/main.js'],
  onShellReady() {
    // HTML shell 就绪,开始流式传输
    pipe(res)
  }
})

// Selective Hydration — Suspense 边界实现
<Suspense fallback={<Loading />}>
  <Comments /> {/* 延迟水合 */}
</Suspense>

水合是 SSR 的隐藏成本——HTML 快速显示改善了 LCP,但水合期间 JS 执行阻塞主线程,影响 INP。选择 SSR 时不只看 LCP,还要评估水合对交互延迟的影响。Resumability(Qwik)和 Islands Architecture(Astro)是解决水合成本的两种新思路。

构建优化 — 减少产物体积

打包体积优化 — 先分析再优化

先用 webpack-bundle-analyzer 分析包体积,再针对性优化:

策略做法收益
压缩资源HTML: html-webpack-plugin、CSS: css-minimizer-webpack-plugin、JS: terser-webpack-plugin减少文件体积
Tree ShakingWebpack 自带支持,但库本身需 ESM 格式消除未使用代码
动态导入import() 按需加载"大且非首屏必需"的模块减少首屏包体积
更小库替代dayjs 替代 momentlodash-es 替代 lodash直接减少依赖体积
版本升级保持关键库最新,但需评估 breaking change间接减少体积
去除生产日志自定义日志类生产环境关闭 / Webpack 插件移除 console.log减少体积 + 防止泄露

Tree Shaking 不仅取决于 Webpack 配置,还取决于库本身的模块格式。lodash 用 CJS 不支持 Tree Shaking,需用 lodash-es(ESM 格式)替代。生产日志不是简单"删不删"的问题,而是可调试性与性能的平衡——建议自定义日志类,生产默认关闭但可通过浏览器参数打开。

渲染模式选择 — 按场景权衡

方式做法适用场景成本
CSR客户端渲染,JS 生成 DOM不需要 SEO 的应用
SSR服务端直接返回渲染好的 HTMLSEO + 首屏速度要求近乎重构
预渲染构建时生成静态 HTML改动较小的 SEO 方案
SSG静态站点生成,构建时全量生成内容不变的页面
ISR增量静态再生,按需重新生成内容偶尔更新的页面

SSR 近乎重构网站,中途接入成本极高。如果不需要 SEO,客户端渲染即可;想改善 SEO/首屏但改动预算有限,预渲染是低成本替代。不要盲目选择 SSR,而是根据场景权衡。

网络优化 — 减少传输延迟

缓存策略 — 避免重复请求

类型机制响应头适用场景
强缓存不发请求,直接用本地Cache-Control: max-age=31536000, immutable静态资源(带 hash)
协商缓存发请求验证,304 则用本地ETag / If-None-MatchHTML、API 响应
SW 缓存Service Worker 拦截请求Cache API 灵活控制离线方案、PWA
bfcache前进/后退时直接恢复页面浏览器内置导航回退

传输优化 — 减少往返和体积

策略做法收益注意事项
HTTP/2 多路复用单连接并行请求减少连接开销使雪碧图优化过时
HTTP/3(QUIC)UDP + 0-RTT减少握手延迟
Gzip / Brotli 压缩文本资源压缩传输减少传输体积前端打包只是第一步,Nginx 也必须开启
CDN 边缘部署静态资源就近分发降低 TTFB免费 CDN 需考虑兜底方案
103 Early Hints最终响应前发送资源提示提前预加载
减少不必要请求请求去重与按需请求减少无效请求不是盲目减少,而是减少"不必要的"

Gzip 压缩需前后端配合:仅前端通过 Webpack 插件生成 .gz 文件不够,Nginx 也必须开启 gzip 才能生效。CDN 兜底:免费 CDN 链接存在可用性风险,必须有兜底方案。雪碧图已过时:HTTP2 多路复用使雪碧图失去意义,且维护成本高,现代项目不应再使用。

运行时优化 — 减少主线程阻塞

长任务拆分 — 让主线程喘口气

任务超过 50ms 会阻塞主线程,影响 INP。拆分策略:

// scheduler.yield()(新 API)
async function processItems(items) {
  for (const item of items) {
    doWork(item)
    await scheduler.yield() // 让主线程处理其他任务
  }
}

// requestIdleCallback(低优先级)
requestIdleCallback(() => { /* 非紧急任务 */ })

Web Workers — CPU 密集型任务的后台线程

const worker = new Worker('compute.js')
worker.postMessage({ data: largeDataset })
worker.onmessage = (e) => { result.value = e.data }

Web Worker 适合与 UI 无关的长时间运行脚本(大数据计算、图像处理),但不能操作 DOM。通信通过 postMessage,数据传递需序列化。

性能 API 与测量 — 量化优化效果

PerformanceObserver — 监听关键指标

// LCP
new PerformanceObserver((list) => {
  const entries = list.getEntries()
  const last = entries[entries.length - 1]
  console.log('LCP:', last.startTime)
}).observe({ type: 'largest-contentful-paint', buffered: true })

// CLS
let clsValue = 0
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (!entry.hadRecentInput) clsValue += entry.value
  }
}).observe({ type: 'layout-shift', buffered: true })

// INP
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log('INP duration:', entry.duration)
  }
}).observe({ type: 'event', buffered: true })

// 长任务检测
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log('Long task:', entry.duration)
  }
}).observe({ type: 'longtask', buffered: true })
redirectStart → domainLookupStart → connectStart →
secureConnectionStart → requestStart → responseStart(TTFB) →
responseEnd → domInteractive → domContentLoaded → domComplete → loadEventEnd

旧版 window.performance.timing API 将被废弃,应迁移到 PerformanceNavigationTiming API。基于旧 API 的采集库需要升级,新项目应直接使用新 API。

测量工具 — 实验室 + 字段双视角

工具类型用途
Lighthouse实验室综合审计,生成评分和优化建议(建议无痕模式测试)
DevTools Performance实验室帧率分析、长任务检测、渲染管线
DevTools Network实验室瀑布图、缓存分析
WebPageTest实验室多地域、多设备测试,高级瀑布图
PageSpeed Insights实验室 + 字段Lighthouse + CrUX 结合
web-vitals 库字段Google 官方 JS 库,简化 CWV 采集
Search Console字段Core Web Vitals 评估报告
// web-vitals 库采集 + sendBeacon 上报
import { onLCP, onINP, onCLS, onTTFB } from 'web-vitals'

function reportMetric(metric) {
  navigator.sendBeacon('/api/metrics', JSON.stringify(metric))
}

onLCP(reportMetric)
onINP(reportMetric)
onCLS(reportMetric)
onTTFB(reportMetric)

sendBeacon 比 XHR 更适合性能数据上报,因为它不受页面卸载影响,确保数据不丢失。RUM(真实用户监控)反映真实用户体验,合成监控适合回归测试——两者结合才能全面评估。

性能优化体系总结 — 七层金字塔

层级关注点关键手段
网络与传输TTFB、连接开销CDN(带兜底)、缓存、HTTP/2+、Brotli
资源加载FCP、LCPpreload/prefetch、懒加载、代码分割、图片优化
渲染路径阻塞、关键路径内联关键 CSS、defer JS、减少关键资源
框架层CWV 与渲染模型Vue 编译优化、React Lane 调度、选择性水合
运行时INP、长任务批量 DOM、虚拟滚动、合成层、防抖节流、Web Worker
构建包体积压缩、Tree Shaking、动态导入、更小库替代、去日志
测量与监控全指标Performance API、Lighthouse、web-vitals + sendBeacon、RUM

性能是持续过程,不是一次性任务。优先优化对用户感知影响最大的指标,实验室数据(Lab)与字段数据(Field)结合分析,建立性能预算防止回退。

项目表达模板 — 现象 → 定位 → 方案 → 结果

步骤内容举例
现象描述用户感知的性能问题首屏加载 4s+、列表滚动卡顿
定位用指标和工具找到瓶颈Lighthouse 发现 LCP 4.2s、DevTools 定位长任务
方案针对瓶颈的具体优化图片 WebP + preload、代码分割、虚拟滚动
结果用数据验证优化效果LCP 从 4.2s → 1.8s、INP 从 350ms → 120ms

常见误区 — 14 个高频踩坑点

误区正解
setTimeout 做动画requestAnimationFrame
will-change 随处设置只在即将变化时设置,完成后移除
只看实验室数据实验室 + 字段数据结合分析
图片不设尺寸必须设 width/heightaspect-ratio,避免 CLS
CSS 放底部CSS 必须在 <head> 中,否则阻塞渲染
懒加载一切关键资源不应懒加载,首屏英雄图应 preload
盲目减少 HTTP 请求应减少"不必要的"请求,而非盲目减少
Gzip 前端打包即可Nginx 也必须开启 gzip 才能生效
需要 SEO 就用 SSR预渲染是低成本替代,SSR 近乎重构
还在用雪碧图HTTP2 多路复用使雪碧图过时,维护成本也高
moment 处理日期dayjs 体积仅 2KB,moment 230KB+
CSS 选择器需要深度优化最慢和最快差别极小,避免反模式即可
SSR 一定能改善所有指标SSR 改善 LCP 但水合增加 INP,需权衡
框架优化就是加 memo优化要从渲染模型和更新粒度出发,而非到处加缓存

本页内容