前端面试进入“铜九铁十”这个节点,后台收到最多的私信就是问性能优化怎么答。说实话,性能优化在面试里的地位一直很微妙:基础题问烂了,但想把“用过哪些优化手段”聊出信息量,很多同学会卡在“知道一堆名词,讲不清为什么这么做,更拿不出量化结果”。金九银十是求职黄金期,铜九铁十更像前端圈自嘲式的硬核竞争——机会不少,但筛人的问题越来越刁钻。
这篇文章算是我这些年面试候选人和自己准备跳槽时,针对性能优化整理的完整思路。不打算罗列一堆“八股文”背诵点,而是从指标、网络、渲染、运行时、构建、监控、高频追问七个维度,把这条知识链串起来。无论你是刚开始准备面试的应届生,还是想系统补漏的进阶开发,应该都能找到能直接用上的东西。
1. 先把优化目标钉死:性能数字不是背给面试官听的
1.1 Core Web Vitals 与 2026 年的新常态
很多面试者一上来就说“我做过性能优化,做了懒加载、压缩图片、webpack优化”。问题在于,面试官没办法从这些零散动作里判断你是否理解“性能优化到底为了什么”。性能优化的最终目标是用户体验,而体验需要量化。2024年之后,Google 把 Core Web Vitals 的指标体系更新了一轮,最核心的三个变成了 LCP、INP、CLS。
这里有个需要更新的点:以前大家张口就是 FID,但 FID 只度量首次可交互前的输入延迟,且依赖用户真实点击,数据噪声大,后来 Google 用 INP(Interaction to Next Paint)替换了它。INP 采集用户在页面整个生命周期中所有点击、键盘操作的长任务延迟,最终取一个近似 P75 的值作为整体响应能力指标。
面试要记住阈值,但更要理解为什么是这三个:
- LCP(Largest Contentful Paint):最大内容绘制,目标是 2.5 秒以内。它回答“用户能不能看到主要内容”的问题。图片、视频、文字块都可能是 LCP 元素,需要针对具体元素做优化,而不是笼统说“首屏快”。
- INP(Interaction to Next Paint):下一次绘制交互延迟,目标是 200 毫秒以内。它衡量的是页面能不能快速响应用户操作,和 JavaScript 长任务、主线程繁忙强相关。
- CLS(Cumulative Layout Shift):累计布局偏移,目标是 0.1 以内。它衡量视觉稳定性,比如图片没有预留宽高、字体加载导致文字跳动、组件插入导致页面塌陷,都会拉高 CLS。
这三个指标背后对应的是加载性能、交互性能和视觉稳定性,正好覆盖了用户体验最关心的三个维度。我在面试里常让候选人讲“你们项目这三个指标是多少”,大多数人答不上来。如果你能脱口而出自己负责模块的 LCP 是 2.1s、INP 是 180ms、CLS 是 0.05,面试官对你的印象会立刻不一样——因为这代表你真正做过线上测量,而不是只在文档里看过概念。
1.2 实验室数据与现场采集:Lighthouse、WebPageTest 和 Performance API
性能指标怎么来?其实分两类:实验室数据和真实用户监控(RUM)。实验室数据用 Lighthouse 或 WebPageTest 模拟固定网络环境和设备采集,适合开发阶段回归;RUM 则从真实用户浏览器上报,覆盖更真实,但噪声也更多。
Lighthouse 几乎是前端面试必提工具。但很多候选人只知道“跑一下出个分”,说不清分数怎么来的。Lighthouse 本质上是跑一个 Chromium 实例,模拟移动端慢网络(比如 Fast 4G),从导航开始记录性能时间线,再把各类指标乘以权重算出一个综合分。所以你会发现同一页面在不同网络下跑分数差很多,因为模拟环境是一致的,实际页面加载分布则更复杂。
WebPageTest 是更专业的实验室工具,能选全球不同位置的真实浏览器,查看完整的 Waterfall 时间线、TTFB、每个请求的细节,还能做视频回放看页面是怎么逐步渲染出来的。如果你要排查“到底哪个请求拖慢了首屏”,WebPageTest 比 Lighthouse 直观得多。
如果要在代码里做更精细的采集,Performance API 是绕不开的。比如:
const nav = performance.getEntriesByType('navigation')[0]; if (nav) { console.log('DNS耗时:', nav.domainLookupEnd - nav.domainLookupStart); console.log('TCP耗时:', nav.connectEnd - nav.connectStart); console.log('TLS耗时:', nav.secureConnectionStart ? nav.secureConnectionStart - nav.connectStart : 0); console.log('首字节耗时TTFB:', nav.responseStart - nav.requestStart); console.log('DOM完整加载:', nav.domContentLoadedEventEnd - nav.navigationStart); console.log('页面完全加载:', nav.loadEventEnd - nav.navigationStart); }PerformanceObserver 则更适合监控长任务和资源时序,比如用 PerformanceObserver 监听 longtask,把超过 50ms 的任务收集下来上报,这能直接定位主线程瓶颈,也是后面讲运行时优化和监控 SDK 的基础。
1.3 没有性能预算的优化都是耍流氓
给项目定性能预算,是我非常推荐在面试和简历里提到的点。原因是它能把“性能优化”从一次性活动变成可持续流程。
性能预算可以分两类:
- 指标预算:比如 LCP 不超过 2.5s,INP 不超过 200ms,CLS 不超过 0.1。这个可以用 Lighthouse CI 或者自建的 Performance Budget 工具在 CI 里跑。
- 资源预算:比如首屏 JavaScript 体积不超过 170KB(gzip),图片总大小不超过 500KB,第三方脚本不超过 2 个。
有了预算,才能防止“下个版本回归”。我经历过一个项目,做了两个月的性能优化,首屏从 6 秒压到 3 秒,结果新同事加了一个地图 SDK 和一个埋点脚本,又回到 5 秒。后来我们直接在 CI 里加了一个简单的体积检查:超过预算就失败。虽然偶尔会被“关掉”重新调预算,但至少每次都有讨论,而不是无声无息地退化。
这一点面试官很爱听:说明你有工程化思维,不是单纯“调优”。
2. 网络与资源层的提速:连接阶段省下的每一毫秒
2.1 DNS、TCP/TLS 预连接到底该怎么配
面试经常从一个非常经典的环节切入:“从输入 URL 到页面展示,你能说出几个优化点?”大多数人回答到“DNS 解析、TCP 连接、HTTP 请求”,但不知道怎么落地。
DNS 解析在连接链路里的耗时不可忽视。每次域名解析少则几十毫秒,多则上百毫秒。浏览器的 DNS 缓存可以帮助,但首次访问或子域名较多的站点仍然需要实际解析。前端能做的动作是<link rel="dns-prefetch">,让浏览器在后台提前解析关键域名。
但 dns-prefetch 只做 DNS 解析,如果你估计到后面还要建立 TCP 连接甚至 TLS 握手,那就要用 preconnect。<link rel="preconnect">会提前完成 DNS 解析 + TCP 握手 + TLS 握手。尤其你的页面要请求第三方 API、上传下载文件、加载字体或地图等跨域资源时,preconnect 的效果非常明显。
一个典型案例:页面调用同一个云厂商的 API 网关,TTFB 经常在 300ms 以上。加了 preconnect 后,DNS+TLS 环节从关键路径里被隐藏了,TTFB 降到了 180ms 左右。注意 preconnect 也不要滥用,因为它会占用并保持连接,如果后续资源没有使用,反而浪费浏览器的连接池。我一般只对 “首屏一定会请求且尽快需要” 的一两个域名使用。
面试时你可以补充一个点:preload 和 preconnect 容易混淆。preload 是提前下载当前页面需要的某个具体资源,告诉浏览器 “这个任务要优先处理”;preconnect 是提前建立连接,并不下载资源。两者配合得当,能明显改善关键资源的加载顺序。
2.2 HTTP 缓存:强缓存、协商缓存与前端版本号的默契
网络层优化里,缓存是性价比最高的动作。面试必问“强缓存和协商缓存的区别”,但很多人只会背概念。我建议结合工程实践来回答。
强缓存由Cache-Control: max-age=31536000控制,浏览器在 max-age 内不会再发请求,直接走本地缓存。协商缓存则由ETag或Last-Modified控制,浏览器先发一个请求给服务器,服务器判断资源没变,返回 304,响应体很小,但仍然有一次网络往返。
对于静态资源,最佳实践是文件名带 hash,然后设置Cache-Control: max-age=31536000, immutable。因为文件名变化相当于新 URL,浏览器自然发起新请求,不需要关心“缓存更新”问题。带immutable告诉浏览器在有效期内绝对不要去服务端验证。这是前端工程化里最常见的“hash + 长期缓存”模式。
但这里有一个我踩过的坑:很多人只在 Nginx 层配置了etag on和last_modified on,却没有处理好 HTML 的缓存策略。HTML 上设置了max-age=3600,导致 CDN 或浏览器缓存了 index.html,用户部署新版本后,刷新页面拿到的还是旧 HTML,里面的 JS/CSS hash 全是旧的,这就是典型的“缓存吞噬发布”。正确做法是 HTML 设置Cache-Control: no-cache,让它每次回源验证,但静态资源使用长期缓存。
面试追问可能会聊到 Service Worker,你可以顺带提一下stale-while-revalidate:Service Worker 拦截请求后,先返回缓存内容,再在后台更新缓存,下次访问就是新内容。这个策略很适合非首屏接口和图片资源。
2.3 图片字体传输层的“隐身术”:压缩、响应式与三协议
网络资源的主体往往是图片。图片优化的核心不是“压缩一下”,而是按需提供合适尺寸和格式。
响应式图片用srcset和sizes属性,让浏览器根据当前视口宽度选择最合适的图片 URL。如果你用 CDN 且支持图片处理参数,可以在 URL 上动态指定宽度和压缩比例。现代格式方面,WebP 已经是大势所趋,AVIF 更小但要考虑兼容性。我可以提供一个选择逻辑:
- 大尺寸图片、轮播图、背景图:优先考虑 WebP/AVIF,同时保留 JPEG fallback。
- 图标和小 PNG:直接转成 SVG 雪碧图或 iconfont。
- 背景图片:能切图就不要传整张大图,按 1x/2x 输出。
字体加载是常被忽略的 CLS 元凶。字体文件没加载完,浏览器会先用 fallback 字体渲染,等到 Web Font 下载完成再切换,这直接导致文字区域宽度和行高变化。解决方案包括:字体子集化(比如用 fontmin 只保留用到的字符)、使用font-display: swap虽然会闪字体但避免白屏、利用preload加载关键字体文件。更进一步的做法是把字体做 encode 到 CSS 里,但那只适用于小体积字体,通常几百 KB 的字体不建议这么干。
传输层协议方面,HTTP/2 多路复用已经普及,把多个小请求并发到一个连接里。HTTP/3(基于 QUIC)还在逐步普及,面试提到“为什么 HTTP/2 还没彻底解决队头阻塞”可以加分。HTTP/2 的多路复用只在连接层面消除了队头阻塞,但 TCP 层仍有丢包重传的队头阻塞,HTTP/3 改成 UDP + QUIC 后进一步优化。不过面试别硬背,核心是展现你对协议演进的理解能落到资源加载策略上。
3. 渲染链路优化的硬功夫:从 FCP 到 INP 的每一帧
3.1 关键渲染路径:哪些资源在“绑架”首屏
面试题目里最经典的就是“关键渲染路径”。你需要画出一条链路:HTML -> DOM,CSS -> CSSOM,两者合并成渲染树,然后布局、绘制、合成。资源阻塞是重点:
- CSS 是渲染阻塞资源。浏览器在构建 DOM 的过程中遇到
<link rel="stylesheet">会暂停脚本执行,但不会完全阻止 DOM 构建;不过 CSSOM 构建完成之前,渲染树无法构建,所以默认情况下页面不会首帧绘制。因此首屏 CSS 体积必须控制,并用媒体属性media="print"把非视口样式变成非阻塞加载。 - JavaScript 是解析阻塞资源。普通
<script>会暂停 HTML 解析,并且要等前面的 CSSOM 构建完成才能执行。所以常用defer或async。defer保证脚本按顺序在文档解析完成后执行,async一旦下载完就立刻执行,适合完全独立的第三方脚本。 - 字体和图片会触发重排或延迟加载,但不会阻塞首次渲染。
我还遇到过面试官问:“为什么现在 ES Module 里<script type="module">默认是 deferred?”因为模块脚本需要先解析依赖,浏览器将其设计为默认不阻塞 HTML 解析,并且一定会延迟执行,这跟defer的行为一致,但更严格。
优化首屏,常见手段是提取关键 CSS(Critical CSS),把影响首屏样式的 CSS 内联到 HTML 中,其余样式文件交给preload异步加载。不过这个方案在工程化里要权衡维护成本。我一般建议用工具自动生成关键 CSS,并在 CI 里校验体积。
3.2 重排、重绘与合成层的日常操作细节
面试环节喜欢让候选人举例说明“如何避免重排重绘”。这里的关键不是背概念,而是理解浏览器渲染流程。
每次修改 DOM 样式,浏览器都会经过 Style -> Layout -> Paint -> Composite(具体步骤因属性而异)。重排(Layout)代价最高,因为它涉及几何计算,会影响子节点和兄弟节点。重绘(Paint)次之,因为需要绘制像素。合成(Composite)代价最低,因为只把已绘制的图层交给 GPU 交叉合成。
所以优化的基本原则是:能把操作限制在合成层,就不要触发重绘;能重绘,就不要重排。
实操清单:
- 用
transform和opacity做动画,不要用top/left/width/height,前者走合成器,后者走布局+绘制。 - 批量读操作、批量写操作,避免“读-写交替”导致强制同步布局。比如循环里读
offsetWidth再改样式,会造成每一轮都要重新布局一次。 - 需要修改多个样式属性时,用
classList切换一个 CSS class,而不是逐个设置style。 - 大量插入 DOM 时,使用
DocumentFragment或者在display: none的容器里操作,完成后一次性显示。 will-change可以提前提示浏览器生成独立图层,但不能滥用,否则 GPU 内存爆炸。
这套知识在 Vue/React 项目中依然有效,不要以为框架的虚拟 DOM 帮你解决了所有性能问题。虚拟 DOM 优化的是 JavaScript 层面的 diff,但最终还是要操作真实 DOM。如果你在 Vue 里直接document.getElementById('app').innerhtml = ...,再多的虚拟 DOM 也救不回来。
3.3 大数据量渲染:虚拟列表与时间切片
面试常见场景题:“后端返回一万条数据,前端怎么渲染?”
如果你直接渲染一万个 DOM 节点,页面会卡成幻灯片。结合真实项目经验,我会按数据量和交互复杂度分几层回答:
- 最简单的方案是分页或加载更多,后端一次只给一页。
- 如果必须支持滚动连续加载,用虚拟列表:只渲染可视区域内的节点,上下各加一个缓冲区,滚动时通过计算
scrollTop和每项高度来更新渲染范围。核心思路是“窗口化渲染”,而不是把一万个节点全挂上去。 - 如果业务复杂,比如每行有大量子组件和状态,考虑把“行容器”固定高度,再配合
content-visibility: auto让浏览器自动跳过视口外的渲染工作。content-visibility是一个非常实用的 CSS 属性,但要注意它和懒加载、虚拟列表的关系,不要重复使用。
另一个面试热点是“如何解决大数据量一次性渲染导致主线程卡顿”。除了虚拟列表,还可以用时间切片:把任务切成小块,每执行 5ms 左右就让出主线程,让浏览器有机会响应用户输入和渲染帧。基础实现是requestIdleCallback配合工作队列,或者直接setTimeout(…, 0)。
React 里的useTransition和useDeferredValue就是时间切片的框架级实现,底层依赖 Fiber 的可中断渲染。这一块如果能在面试中自然带出来,说明你不仅知道手写性能方案,还理解现代框架的设计方向。
4. JavaScript 运行时和内存深水区:别把主线程跑满
4.1 事件循环与长任务:让出主线程比写得更快更有效
前端性能的瓶颈越来越集中在“主线程过忙”。浏览器的主线程既要执行 JavaScript,又要处理样式、布局、绘制,还要响应键盘鼠标事件。一旦某个任务执行时间超过 50ms,它就被称为“长任务”,用户会明显感觉到卡顿。
优化长任务的核心不是“把函数写得更快”,而是“把任务切开,让出主线程”。常见手段:
setTimeout(() => chunks[i++], 0)把大循环拆成多个宏任务。- 用
MessageChannel做微任务/宏任务级别的调度,性能比setTimeout(0)更好,因为setTimeout嵌套超过 5 层后会有至少 4ms 的节流延迟。 - 现代浏览器支持
scheduler.postTask和scheduler.yield,可以按优先级调度,但目前兼容性仍需关注。 - 使用 Web Worker 把纯计算任务放到独立线程,比如大量数据处理、Canvas 像素计算、文件上传前的 hash 计算、JSON 解析等。注意 Web Worker 里不能操作 DOM,但可以和主线程通过
postMessage通信。
面试官问到“Web Worker 在什么场景用过”,不要说“我了解”,最好能给出一个具体场景。比如我做过“前端使用 Worker 上传大文件分片”:主线程读取文件分片,把每个分片交给 Worker 计算 hash,Worker 再把 hash 返回主线程触发上传请求。这样主线程不会因为计算 hash 卡死,用户还能继续操作页面。
4.2 V8 的一些“潜规则”与防抖节流手写题
V8 引擎对 JavaScript 的优化也有“反直觉”的地方。我整理几个面试可以用上的点:
- 对象形状(hidden class)。V8 会为对象动态隐藏类。如果你频繁给对象增减属性,会导致隐藏类变换,降低内联缓存命中率。所以推荐初始化时就把所有属性定义好,不要再
delete obj.name。 - 数组越界和稀疏数组。越界读写会导致数组变成字典模式,性能骤降。不要随意留洞,比如
arr[1000] = 1会让 V8 放弃快速数组。 - 函数优化。V8 会对高频函数做内联缓存,但如果你参数类型不稳定,比如同一个函数一会儿传字符串一会儿传数字,优化就会失效。TypeScript 的好处不只是类型提示,还在于帮助避免“隐藏类”破坏。
防抖节流是“前端面试手写题”里的常客。很多人会背代码,但面试官更想听边界情况。防抖的核心是“事件触发后等待一段时间再执行,如果期间再次触发则重新计时”,节流是“固定时间间隔内只执行一次”。
防抖实现时要注意“立即执行”选项和“取消”能力。节流则有两种方式:时间戳版(立即执行)和定时器版(延迟执行),各有副作用。我在实际项目里更推荐基于requestAnimationFrame实现节流,因为 rAF 在页面不渲染时会自动暂停,更适合动画和滚动场景。能写出带leading和trailing控制的版本,会是一个很好的加分项。
4.3 内存泄漏排查:从 DevTools 到真实案例
内存泄漏会让页面越用越卡,在移动端更容易触发崩溃。面试问“如何排查前端内存泄漏”时,不要只说“用 Chrome DevTools 看 Heap Snapshot”,而要描述一套可执行的排查流程:
- 打开 Performance Monitor,观察 JS 堆大小和 DOM 节点数是否随时间持续上涨。
- 用 Memory 面板录制 Heap Snapshot,过滤
Detached。如果出现大量分离的 DOM 节点,基本可以确定是事件监听器或闭包引用导致节点无法回收。 - 结合 Performance 面板的长任务记录,找哪些事件触发了大量内存分配。
常见泄漏源头我总结过:
- 全局变量。
window.foo = data忘了清理。 - 定时器和定时器里引用的外部对象。比如组件卸载后
setInterval还在执行,回调里的 DOM 引用就永远不会释放。 - 事件监听器未移除。尤其自定义事件、
window.addEventListener('resize', handler)但组件销毁时没有removeEventListener。 - 闭包持有一棵大型 DOM 树。
- React/Vue 中未清理副作用。React 的
useEffect如果设置了定时器,就必须在 cleanup 里清除。Vue 的onUnmounted同理。
此外,2023 年之后的浏览器新增了WeakRef和FinalizationRegistry,可以排查对象是否被回收。不过这个 API 目前更适合框架级工具使用,普通业务代码不建议碰,容易增加心智负担。
5. 构建与工程化:把性能优化嵌进发布流程
5.1 包体积从 3MB 到 1MB,我做了哪几件事
构建层优化是面试“项目难点”的最佳素材。我拿一个真实案例拆解:某个后台管理系统首屏 JavaScript(gzip 前)3.2MB,LCP 长期在 4.5s 左右。我做了四件事,最终 1.1MB,LCP 降到 2.1s。
第一,用webpack-bundle-analyzer(如果是 Vite 可以用rollup-plugin-visualizer)看体积分布。我发现moment占了 400KB,echarts全量引入占 800KB,还有几个工具库被重复打包。于是把moment替换成dayjs(体积减少 90%),echarts改成按需引入,只保留用到的折线图和柱状图模块。第二,检查有没有重复的依赖版本,通过resolve.alias或peerDependencies强制统一。第三,把 React/Vue 这类基础库改成 CDN 引入,并在externals里声明,这样项目的 vendor 包可以瘦身一大块。但要注意,CDN 引入会带来第三方不可控和服务稳定性问题,我建议只在公司内部自建 CDN 或选择可靠性很高的公共 CDN 时做。第四,对路由做代码分割,每个路由单独打包,并配合prefetch预加载下一个可能访问的页面。
关键点是每个动作都要有数据依据,不要“凭感觉拆”。面试官问你为什么不用xxx,你要能说出当时的取舍。
5.2 Tree Shaking、sideEffects 与按需加载的坑
Tree Shaking 是大家都知道的构建优化,但实操里经常失效。它的原理是基于 ES Module 的静态结构,在打包时删除未被引用的导出。默认情况下,webpack 和 Vite 都会做 Tree Shaking,但有一个隐藏条件:模块必须是纯 ES Module,且引入方式用具名导出。
常见失效场景:
- 引入 CJS 模块(如
const lodash = require('lodash')),无法静态分析。 - 包的
package.json没有配置sideEffects: false,编译器不敢删除可能导致副作用的模块。 babel转译后把 ES Module 变成了 CommonJS。所以在 babel 配置里,设置@babel/preset-env的modules: false,保留 ES Module 语法。- 像
lodash这样的大型工具库,即使import { debounce } from 'lodash',仍然可能把整个库打包进去。解决方案是直接用路径引入import debounce from 'lodash/debounce',或使用lodash-es。
按需加载(code splitting)方面,除了路由级拆分,组件拆分也很重要。React 用React.lazy+Suspense,Vue 用defineAsyncComponent,核心都是“需要时才加载”。但代码分割粒度不能过细,否则会产生大量小请求,反而降低加载性能。实践中,我一般只对“体积大、非首屏、进入频率低”的资源做单独拆包,像图标库、富文本编辑器、地图 SDK 这类都值得拆。
5.3 微前端/模块联邦场景下的性能取舍
前端工程化发展到今天,微前端也是热门话题。搜索热词里就有“微前端”,而它和性能优化不是脱离的。微前端的核心挑战是“应用间共享依赖”和“避免重复加载”。
Module Federation(模块联邦)是 Webpack 5 推出的能力,允许多个独立应用在运行时共享模块。比如主应用把react、react-dom暴露出去,子应用就可以不打包 react,直接从主应用获取。这样可以显著降低子应用的体积。但要注意,模块联邦也会带来运行时解析和版本同步的复杂性,本地调试和部署都要调整。
微前端场景下的性能优化,还需要考虑“子应用加载时机”。通常可以按路由匹配去预加载子应用,而不是首屏一次性加载全部。qiankun或wujie这类框架都有预加载配置,但预加载会占用带宽和 CPU,需要根据业务权衡。如果你能在面试中提到“微前端并非银弹”,就说明你做过工程架构的决策,而非只停留在 API 使用。
6. 性能监控与团队落地:防止下个版本一夜回到解放前
6.1 性能监控 SDK 的“最小可用版本”
性能优化不能“优化完就完了”,一定要有监控。很多团队没有自建监控体系,面试官也未必要求你会写整套 APM,但至少要知道一个最小可用方案怎么设计。
我建议用 Performance API + PerformanceObserver + sendBeacon 组合:
- 导航计时:从
performance.getEntriesByType('navigation')[0]拿 TTFB、DOMContentLoaded、Load。 - 资源计时:从
performance.getEntriesByType('resource')拿每个脚本、图片、接口的具体耗时,可以分析慢资源。 - 最大内容绘制和布局偏移:用
PerformanceObserver观察largest-contentful-paint和layout-shift。 - 长任务:观察
longtask,超过 50ms 就上报。 - 错误捕获:
window.onerror和unhandledrejection。
上报时要注意不能影响性能本身,所以用navigator.sendBeacon()在页面visibilitychange或退出时发送。同时做好采样,比如只上报 10% 的会话,避免打爆后端。
真实用户数据(RUM)和实验室数据要对比看。Lighthouse 显示你的评价是 90 分,不代表全量用户都快。用户网络、设备、浏览器、地理位置都会影响真实体验。所以面试时如果能主动区分“实验室数据 vs RUM”,会显得更专业。
6.2 面试必问的项目陈述:怎么讲清楚一次优化
这是很关键的一节。面试官喜欢问“你在项目中做过哪些性能优化”,不是想听“我用了懒加载”,而是想听到一个有前因后果的技术 story。
我建议用 STAR 法则组织回答:
- S(背景):项目是一个 B 端报表系统,页面包含大量图表和数据表格,用户反馈首次打开要等 4~5 秒。
- T(目标):把首屏 LCP 降到 2.5s 以内,CLS 小于 0.1,JS 体积减小 50%。
- A(行动):先量化瓶颈,通过 Lighthouse/WebPageTest 发现首屏最大问题是全量引入 ECharts、moment 打包过大,以及多个第三方 SDK 未异步加载。然后拆包、按需引入、把非首屏图表用动态加载,改 SSR 或预渲染等。加上 CDN 缓存策略和图片格式优化。
- R(结果):LCP 从 4.5s 降到 2.1s,JS 体积从 3.2MB 降到 1.1MB,线上 RUM 数据 P75 达标。
注意,回答时不要夸大数据,面试官如果追问“你怎么知道 LCP 是 4.5s”,你能说出工具和当时的截图,才有说服力。数据背后最好有监控体系支撑,这样就形成了闭环。
7. 高频追问与答题避坑:听起来对但经不起问的部分
7.1 从“输入 URL 到页面展示”延伸出的优化追问
这道题几乎是前端面试的“必考大题”,一旦展开到性能优化维度,问题会非常多。你可以这样组织主链路:
- 用户输入地址,浏览器进行 DNS 解析,命中缓存则快,不命中则去本地 DNS 服务器递归查询。可优化点:DNS 预解析、CDN 动态加速。
- 建立 TCP 连接,可能还叠加 TLS 握手。可优化点:preconnect、HTTP/3。
- 发送 HTTP 请求,服务端处理,返回 HTML。可优化点:CDN 加速静态资源、服务端渲染(SSR)、边缘函数(Edge Functions)减少首跳延迟。
- 浏览器解析 HTML,发现外部资源。可优化点:预加载关键资源(preload)、预连接第三方域名。
- 构建 DOM 和 CSSOM,执行 JavaScript。可优化点:内联关键 CSS、defer/async 脚本、代码压缩、移除阻塞渲染资源。
- 渲染页面,用户开始交互。可优化点:减少重排重绘、优化长任务、内存管理、性能监控。
面试官可能会深挖某一环,比如“为什么 preload 可以快到关键资源?”,“SSR 性能就一定比 CSR 好吗?”你要能说清楚 SSR 的优势是首屏 HTML 直接返回,劣势是服务端渲染压力和 TTFB 可能更高,所以不是万能答案。
7.2 经典误区:合并请求、JS 底部加载、CDN 缓存一切
很多备考资料里写“减少 HTTP 请求数,合并 JS/CSS”,这放在 HTTP/1.1 时代是对的,但在 HTTP/2 时代要打问号。HTTP/2 多路复用让多个请求可以共用一个 TCP 连接,过度合并文件反而破坏缓存粒度。比如你合并了四个页面共用的工具库和首页专用模块,首页改了,整个文件缓存失效,其他页面也要重新下载。所以现在更推荐“合理拆分 + 缓存友好”,而不是盲目合并。
另一个常见误区是“JS 应该放在 body 底部”。现在更现代的做法是使用defer或async,放头部也可以,关键是不要阻塞 HTML 解析。defer脚本在 HTML 解析完成后执行,async下载完立刻执行。放在 body 底部只是“兼容旧写法”的替代方案,谈不上最佳实践。
关于 CDN,很多人觉得“CDN 就是缓存一切”。实际上,CDN 缓存导致更新延迟,静态资源可以通过 hash 文件名解决,但接口请求和 HTML 不能简单缓存。另外还有 CDN 回源、跨域、Preflight 请求等问题。回答这个话题时,要体现出“缓存是有策略的,不是一刀切的”。
7.3 现场评估题:白屏和卡顿的排查思路
面试最后可能会给一个场景:“用户反馈某个页面打开白屏,你怎么排查?”这个问题的开放性很强,但如果你有条理地按步骤回答,就能拿高分。
第一步,先确认现场。如果没有线上监控,找运营或用户拿 UA、设备、网络、复屏路径。第二步,打开 DevTools Network 看资源加载顺序,是 HTML 没回来、CSS/JS 挂了,还是接口错误。第三步,根据白屏范围区分:全站白屏大概率是公共基础库或入口脚本挂了;单页面白屏可能是路由配置、鉴权失败或该页面拉数据异常。第四步,如果资源正常,但白屏,用 Performance 面板看 JS 执行时间和是否有报错,使用 Console 面板直接看 exception。如果 JS 执行过长,也要考虑主线程长任务阻塞导致渲染被推迟。整个过程要体现“从现象到数据到原因”的排查思路。
卡顿类场景类似,但要侧重主线程:先看长任务,再看是否有大量 layout 和 paint,最后检查内存泄漏。如果你能在现场快速定位到某个具体组件或第三方脚本,那基本就稳了。
最后说一个我自己的习惯。平时写代码时,我会在浏览器控制台跑一段简单脚本,记录performance.getEntriesByType('resource')里耗时超过 500ms 的资源,同时看Largest Contentful Paint和First Contentful Paint。遇到性能问题,先复现、量化,再动手。铜九铁十的面试也一样,真正让你拿到 offer 的不是你背了多少条优化措施,而是你能不能把一条优化讲出闭环:指标观察到瓶颈,方案落到代码,数据验证收益,流程防止回退。祝你顺利。