问题场景 → 优化思路 → 关键代码 → 效果验证 · 配 Lighthouse / Performance 实战图
面试官最爱问的开场题。不要只说"提升体验",要能说出具体数字和业务影响。
| 场景 | 数据 | 来源 |
|---|---|---|
| Amazon 页面慢 100ms | 销售额下降 1% | Amazon 研究 |
| Google 搜索慢 400ms | 搜索量下降 0.59% | Google 研究 |
| Walmart 页面快 1s | 转化率提升 2% | Walmart 数据 |
| Pinterest 首屏快 40% | 注册提升 15% | Pinterest 工程 |
Chrome DevTools 自带 Lighthouse,一键跑出性能报告。下面是一个真实页面的评分截图模拟:
Lighthouse 面板Mobile(手机端更严格,能暴露更多问题)Generate report,等 10-30 秒出报告Google 定义的核心 Web 指标,直接影响搜索排名。面试必问,三个指标要能背出来。
下面是优化前后的指标对比,直观感受"好"和"差"的差距:
| 指标 | 含义 | 优秀 ✅ | 需改进 ⚠️ | 差 ❌ |
|---|---|---|---|---|
| LCP 最大内容渲染 |
页面最大元素(如 banner、大标题)渲染完成的时间 | ≤ 2.5s | 2.5s ~ 4s | > 4s |
| INP 交互延迟 |
用户点击/输入后,到页面响应的延迟(2024 替代 FID) | ≤ 200ms | 200ms ~ 500ms | > 500ms |
| CLS 布局偏移 |
页面加载过程中,元素位置突然跳动(点错按钮的元凶) | ≤ 0.1 | 0.1 ~ 0.25 | > 0.25 |
📱 手机 App 类比:
LCP = 你打开 App,主界面(首屏大图)多久能完整显示出来。等超过 2.5 秒你就会觉得"这 App 卡"。
INP = 你点了"提交"按钮,App 多久能给你反馈(转圈或弹提示)。点下去卡 300ms+ 就很膈应。
CLS = 你正要点某个按钮,结果按钮突然往下跳了一下,你点到了上面的广告——这种"布局乱跳"就是 CLS 差。
用户打开页面看到一片白,等几秒才出内容——这是 SPA 最常见的性能问题。面试必问"白屏怎么优化"。
<div id="app">)在 HTML 里放占位灰色块,JS 加载完再替换。用户看到"内容正在加载"而不是白屏。
首屏只加载首页代码,其他页面按需加载。Bundle 从 1MB 降到 200KB。
服务器压缩 JS/CSS,传输体积减少 70%+。1MB JS 传输时只有 300KB。
静态资源放 CDN 节点,用户从最近的节点下载,比从源站快 3-5 倍。
服务端直接输出 HTML 内容,浏览器拿到就能显示,不依赖 JS 执行。Nuxt.js/Next.js。
用 <link rel="preload"> 提前下载关键资源(首屏字体、主 JS),不等 HTML 解析完。
<div id="app"> 里预先放好灰色占位块(骨架屏),JS 加载完后 Vue 会自动替换掉。import() 把每个路由拆成独立的 chunk,首屏只加载首页 chunk,其他页面点进去时才加载。import() 会被打包工具识别,自动代码分割。
loading="lazy")srcset)页面加载完后,用户操作时的卡顿问题。重点:虚拟列表、防抖节流、重排重绘。
startIndex = scrollTop / itemHeight,endIndex = startIndex + 可见行数
transform 代替 top/left 做动画——transform 只触发合成,不触发重排!
| 操作 | 触发 | 优化方式 |
|---|---|---|
| 修改 width/height/top/left | 重排 + 重绘(代价高) | 用 transform 代替 |
| 修改 color/background | 只重绘(代价中) | 用 CSS 变量统一管理 |
| 修改 transform/opacity | 只合成(代价低) | 动画首选这两个属性 |
| 频繁读 offsetHeight | 强制同步布局 | 批量读,再批量写 |
debounce 一个函数,结果整个 lodash(70KB+)都被打包进去了。import/export),不能用 CommonJS(require)。因为 ES Module 是静态的,打包时能分析依赖关系。
| 策略 | 说明 | 适用场景 |
|---|---|---|
| 路由级别分割 | 每个路由一个 chunk | 所有 SPA 项目 |
| 组件级别分割 | 弹窗/编辑器等重组件单独打包 | 大组件(>50KB) |
| Vendor 分割 | 第三方库单独打包(长期缓存) | 所有项目 |
manualChunks 把第三方库按类别拆分——vue 全家桶一个 chunk、UI 库一个 chunk、工具库一个 chunk。业务代码变了不影响 vendor chunk 的 hash,用户缓存还能用。
面试高频题:"强缓存和协商缓存的区别?"要能说清楚。
Cache-Control: max-age 控制。ETag / If-None-Match 控制。| 类型 | HTTP 头 | 特点 | 适用资源 |
|---|---|---|---|
| 强缓存 | Cache-Control: max-age=31536000 | 不发请求,直接用 | JS/CSS/图片(带 hash 文件名) |
| 协商缓存 | ETag / If-None-Match | 发请求,服务器判断 | HTML 文件(入口文件) |
app.a3f5b2.js),内容变了 hash 就变,等于新文件名。app.b8c9d1.js)app.b8c9d1.js 没缓存 → 下载新文件app.b8c9d1.js 强缓存命中,直接用(1 年内都有效)真实可复现的优化路径。面试时讲这种有数字的案例,比泛泛说"我做过性能优化"有说服力 100 倍。
• LCP:4.2s • FCP:2.8s • TTI:5.1s • Bundle:1.8MB
• 接口 RT:1.2s • DOM 节点:~8000 个 • 请求数:43 个
用户反馈:"页面太慢了,等半天还在转圈",跳出率 62%
loading="lazy" 懒加载srcset 响应式图片,手机不加载大图preload
passive: truetransform 替代 top/left(避免重排)
LCP(最大内容渲染)
4.2s ❌
Bundle Size
1.8MB
DOM 节点数
~8000
请求数
43 个
跳出率
62%
LCP(最大内容渲染)
1.2s ✅
Bundle Size
420KB
DOM 节点数
~200
请求数
12 个
跳出率
28%
"我没做过监控"——这是很多人的痛点。本节从零开始,手把手教你用工具发现、定位、分析性能问题。分本地分析和线上监控两条线。
| 对比 | 本地分析(开发时用) | 线上监控(上线后用) |
|---|---|---|
| 目的 | 定位具体瓶颈在哪个函数/请求 | 监控真实用户体验,发现回归 |
| 工具 | Lighthouse / DevTools Performance / Network | Performance API / Sentry / Web Vitals |
| 数据来源 | 你的电脑跑一次 | 所有真实用户的浏览器上报 |
| 使用时机 | 开发阶段、排查问题时 | 7x24 持续运行 |
https://shop.example.comF12(Mac: Cmd+Option+I)打开 DevToolsLighthouse(如果看不到,点 ⟩⟩ 展开更多标签)Mobile(手机端更严格,推荐先跑手机)或 DesktopGenerate report,等 10-30 秒Performance 标签●(录制按钮),变红表示开始录制● 停止录制,生成时间轴 + 火焰图Summary 看 5 大分类耗时,Bottom-Up 看哪个函数耗时最多renderList() ×8000 是瓶颈,渲染了 8000 个 DOM 节点
Bottom-Up 看它调用了哪些子函数,逐层钻取定位renderList() ×8000 宽达 18%,说明渲染了 8000 个节点 → 该上虚拟列表了
Network 标签Disable cache(禁用缓存,模拟首次访问)Cmd+R),看请求瀑布图Timing 标签看时间拆解Size 列:如果有 (from cache) 说明缓存生效;如果传输太大考虑压缩Timing 标签,能看到:遇到性能问题,先对号入座,再用对应工具排查。
症状:用户打开页面,几秒一片白,或首屏大图/大标题迟迟不显示
排查工具: Lighthouse Network 面板
排查步骤:
1. 跑 Lighthouse,看 LCP 数值和 Diagnostics 建议
2. Network 面板看瀑布图:哪个请求最长?(通常是 app.js 太大或图片太大)
3. 如果是 JS 大 → 路由懒加载 + Tree Shaking + Gzip
4. 如果是图片大 → WebP + 懒加载 + 响应式
5. 如果是接口慢 → 推动后端加缓存 / 优化 SQL
症状:点击按钮后要等一会才有反应,滚动不流畅,输入有延迟
排查工具: Performance 面板
排查步骤:
1. Performance 面板录制 → 操作(点击/滚动)→ 停止
2. 看主线程有没有红色长条(长任务 >50ms)
3. 点击长条 → Bottom-Up 看哪个函数耗时
4. 常见原因:大列表全量渲染(→ 虚拟列表)、事件没防抖节流、同步计算太重(→ 用 Web Worker 或 requestIdleCallback)
症状:页面加载时元素突然跳动,用户点错按钮
排查工具: Lighthouse Performance → Layout Shifts
排查步骤:
1. Lighthouse 看 CLS 数值
2. Performance 面板录制 → 看紫色 Layout Shift 条状
3. 点击 Layout Shift 条 → 看 Moved from / Moved to,知道哪个元素跳了
4. 常见原因:图片没设宽高、字体加载跳动、异步 DOM 插入
症状:刚打开很流畅,用 10 分钟后越来越卡,刷新后又好了
排查工具: Memory 面板 Performance Monitor
排查步骤:
1. F12 → Memory 标签 → 选 Heap snapshot
2. 操作前拍一次快照(Snapshot 1)
3. 操作页面 5 分钟(如反复打开/关闭弹窗)
4. 再拍一次快照(Snapshot 2)
5. 选 Comparison → 对比 Snapshot 1 和 2,看哪些对象只增不减
6. 常见原因:定时器没清除、事件监听没移除、闭包持有大对象、Vue 组件卸载没清理
症状:本地测试一切正常,用户反馈偶尔很慢
排查工具: Sentry Performance API 上报
排查步骤:
1. 看 Sentry Performance 面板,按页面/路由筛选慢请求
2. 查慢请求的 Trace(完整调用链),看哪一段最慢
3. 按设备/浏览器/网络维度分组,看是不是特定人群慢(如低端安卓 + 弱网)
4. 如果是接口慢 → 看后端 APM(如 SkyWalking / 阿里 ARMS)
5. 设置告警:LCP >4s 自动通知
| 指标 | 告警阈值 | 级别 | 通知方式 |
|---|---|---|---|
| LCP (P75) | > 4s | P0 紧急 | 电话 + 钉钉 |
| LCP (P75) | > 2.5s | P1 警告 | 钉钉 |
| JS 错误率 | > 0.1% | P0 紧急 | 电话 + 钉钉 |
| 接口 5xx 率 | > 1% | P0 紧急 | 电话 + 钉钉 |
| CLS (P75) | > 0.25 | P1 警告 | 钉钉 |
点击展开看答案。这些问题覆盖了 90% 的前端性能面试题。
Google 定义的核心 Web 指标,直接影响搜索排名。三个指标:
1. LCP(最大内容渲染):页面最大元素渲染完成时间,目标 ≤2.5s
2. INP(交互延迟):用户交互到页面响应的延迟,目标 ≤200ms(2024 年替代 FID)
3. CLS(累积布局偏移):页面元素位置跳动程度,目标 ≤0.1
6 种方案:
1. 骨架屏:HTML 里放占位块,JS 加载前用户看到"正在加载"
2. 路由懒加载:首屏只加载首页代码,Bundle 从 1MB → 200KB
3. Gzip/Brotli 压缩:JS 传输体积减少 70%+
4. CDN 加速:静态资源从最近节点下载
5. SSR:服务端渲染,不依赖 JS 执行(Nuxt/Next)
6. preload 预加载:提前下载关键资源
强缓存:浏览器直接用本地缓存,不发请求。通过 Cache-Control: max-age 控制。适合带 hash 的 JS/CSS/图片。
协商缓存:发请求问服务器资源变没变,没变返回 304(用缓存),变了返回 200(新资源)。通过 ETag / If-None-Match 控制。适合 HTML 入口文件。
最佳实践:HTML 协商缓存(每次验证),JS/CSS/图片强缓存 1 年(文件名带 hash,内容变 hash 就变)。
开发环境:Vite 用原生 ESM,浏览器直接按需请求模块,不需要打包。Webpack 要先打包成一个 bundle 再启动。
预构建:Vite 用 esbuild(Go 写的,比 JS 写的快 10-100 倍)预构建依赖。
HMR:Vite 的热更新只更新改动的模块,不影响其他。Webpack 要重新构建整个 chunk。
生产构建:Vite 用 Rollup,Tree Shaking 更彻底,代码更小。
Tree Shaking = 打包时自动删除未使用的代码,"摇掉"没用的 export。
为什么需要 ES Module:ES Module 是静态的,import/export 在编译时就能确定依赖关系,打包工具能分析出哪些 export 没被使用。而 CommonJS 的 require 是动态的,运行时才知道引用什么,无法静态分析。
注意:用 lodash 时要引入 lodash-es 才支持 Tree Shaking,普通的 lodash 不支持。
防抖(debounce):停止操作一段时间后才执行。期间再次操作会重新计时。
→ 场景:搜索框输入(用户停下来 300ms 才发请求)、窗口 resize
节流(throttle):固定间隔执行一次,不管期间触发多少次。
→ 场景:滚动事件(每 100ms 执行一次)、鼠标拖拽
类比:防抖 = 电梯门(有人进就重新等);节流 = 地铁发车(固定间隔发一班)。
重排(Reflow):改变元素几何属性(宽高、位置),浏览器重新计算布局。代价高。
重绘(Repaint):只改变外观(颜色、背景),不改变布局。代价中。
减少方法:
1. 动画用 transform/opacity(只触发合成,不重排)
2. 批量读写 DOM(不要读写交替,会强制同步布局)
3. 用 DocumentFragment 批量插入 DOM
4. 复杂容器用 contain: layout style paint 隔离渲染
原理:只渲染视口能看到的几行(如 20 行),上下用空白撑起滚动条高度。滚动时动态替换可见内容。
关键计算:startIndex = scrollTop / itemHeight,endIndex = startIndex + 可见行数
场景:1000+ 条数据的列表/表格,如商品列表、聊天记录、日志查看。
效果:DOM 从 10000 个 → 20 个,滚动丝滑,内存降 95%。
推荐库:react-window、vue-virtual-scroller。
preload:当前页面马上要用的资源,高优先级提前加载。如首屏字体、主 JS。
prefetch:下个页面可能要用的资源,低优先级空闲时加载。如用户可能点进去的下个页面 chunk。
一句话:preload = 现在就要;prefetch = 待会可能要。
另外 dns-prefetch 提前解析域名,preconnect 提前建立连接(DNS+TCP+TLS),适合第三方域名资源。
4 招组合:
1. 格式升级:PNG/JPG → WebP,体积小 25-35%
2. 懒加载:loading="lazy",视口外不加载
3. 响应式:srcset + sizes,手机加载小图
4. CDN:图片走 CDN,从最近节点下载
效果:图片加载从 3.5s → 0.8s,流量省 60%。
1. Performance API:用 PerformanceObserver 采集 LCP/INP/CLS,上报到监控平台
2. Sentry:错误监控 + 性能监控,支持回放
3. Lighthouse CI:每次发版自动跑分,防止性能回退
4. 告警:LCP >4s 告警(P0),JS 错误率 >0.1% 告警(P0)
关键:监控不是目的,出问题能第一时间发现、快速定位才是。
常见原因:图片没设宽高、字体加载导致文字跳动、异步插入的 DOM 把已有内容挤开。
优化方法:
1. 图片/视频必须设宽高(或用 aspect-ratio),浏览器提前预留空间
2. 字体用 font-display: swap + 预加载
3. 动态内容用固定高度容器,避免挤压已有元素
4. 避免在已有内容上方插入新内容
代码分割:把一个大的 bundle 拆成多个小 chunk,按需加载。
三种方式:
1. 路由级别:每个路由动态 import(),访问时才加载
2. 组件级别:弹窗/编辑器等重组件单独打包
3. Vendor 分割:第三方库单独打包,用 manualChunks 配置
效果:首屏只加载 200KB,而不是整个 1.8MB。
模板(套数字):
"电商商品列表页,通过 Lighthouse 发现 LCP 4.2s。排查发现三个问题:① Bundle 1.8MB(未做路由懒加载、lodash 全量引入)② 图片未优化(PNG 未转 WebP)③ 列表全量渲染 8000 个 DOM。
优化方案:路由懒加载 + Tree Shaking(Bundle 1.8MB→420KB)、图片转 WebP + 懒加载(图片加载 3.5s→0.8s)、虚拟列表(DOM 8000→200)。
结果:LCP 4.2s→1.2s,跳出率 62%→28%,转化率提升 34%。"
关键:有数字、有思路、有结果。不要泛泛说"做过性能优化"。
1. 度量先行:不要凭感觉优化,用 Lighthouse / Performance API 数据说话
2. 抓主要矛盾:80% 的性能问题来自 20% 的代码,先优化最大的瓶颈
3. 持续监控:优化不是一次性的,要接监控防止性能回退
分两层:
① 自建监控:用 web-vitals 库采集 LCP/INP/CLS,通过 PerformanceObserver 监听长任务,fetch 上报到后端,自建看板展示 P75 趋势。
② 第三方服务:Sentry / 阿里 ARMS / 腾讯 CDC。开箱即用,自动采集 Web Vitals + JS 错误 + 慢请求 Trace。
关键:用 P75 而不是平均值。P75 = 75% 用户体验 ≤ 这个值,更能反映真实情况。设置告警:LCP >4s P0 紧急。
Lighthouse:跑一次模拟访问,给出评分 + 指标 + 优化建议清单。适合"体检",告诉你什么慢和怎么改。
Performance 面板:录制你的真实操作,生成时间轴 + 火焰图。适合"钻取",告诉你哪个函数慢。
配合使用:Lighthouse 发现 TBT 高 → Performance 面板录制 → 看火焰图哪个 JS 函数最宽 → 针对性优化。
症状:页面越用越卡,刷新后又好了。
排查步骤(Memory 面板):
1. F12 → Memory → 选 Heap snapshot
2. 操作前拍快照 1
3. 操作页面 5 分钟(如反复打开/关闭弹窗)
4. 拍快照 2
5. 选 Comparison 对比,看哪些对象只增不减
常见原因:定时器没 clearInterval、事件监听没 removeEventListener、闭包持有大对象、Vue 组件卸载没清理 onUnmounted。