腾讯前端面试:如何排查页面卡顿及链路追踪?

这个问题来源于一次腾讯前端实习岗的面试。这个问题是这样的:

面试官问:

问题一:这里现在有 A、B、C 三个组件,用户反馈很卡。卡顿的原因可能是首屏加载卡,也可能是分页切换卡,还可能是加载数据卡顿。我们应该怎么排查是哪个原因?

问题二:如果 A 组件是一个筛选组件,当 A 组件改变了筛选项,导致 B 列表、C 统计图组件的更新突然变卡,请从 Vue 执行的链路来分析整个过程。

问题一解答

问题一:这里现在有 A、B、C 三个组件,用户反馈很卡。卡顿的原因可能是首屏加载卡,也可能是分页切换卡,还可能是加载数据卡顿。我们应该怎么排查是哪个原因?

  1. 首屏卡

​可能原因​​:

  • 所有组件同步加载,未做代码分割
  • 初始数据请求过多或串行
  • 大型第三方库(如 ECharts、Monaco Editor)未异步加载
  • 大量图片未懒加载,或图片未压缩
  • 组件内 setup 中有同步耗时操作(如遍历大数组) (这个原因在真实业务开发中遇到的比较少,一般存在于库或者工具的开发)

​排查工具​:

  • Chrome Performance:录制首屏,查看 Long Task 分布,哪个函数占用时间长
  • Network:查看瀑布图,是否存在串行请求、大体积 JS bundle
  • Vue Devtools → Timeline:查看组件挂载顺序和耗时

​解决方案​:

  • 路由懒加载 () => import('./B.vue')
  • 异步组件 <Suspense> + defineAsyncComponent(对非首屏关键模块做 code splitting 或者 async component,首屏关键路径上的资源反而要尽可能减少异步加载,否则可能会让首屏更慢)
  • 图片懒加载 v-lazy / loading="lazy"
  • 将初始化请求改为并行 Promise.all
  • 使用 shallowRef 避免深层响应式初始化开销 (主要用于库或者工具开发的优化,真正业务开发中使用极少)

这么多条目记不住怎么办?所以我更希望从一条完整的请求渲染链路来回答。从发送网络请求到 JS 执行,然后 Vue 初始化并渲染页面,紧接着浏览器执行布局、绘制以及合成等操作,最终呈现给用户并可以交互。这里面的每一个细节,其实刚好对应着我们排查的流程点。

我详细说一些关键部分,比如页面出来得晚。举个例子,我们的 JS 下载需要 2 秒钟,接口请求 1 秒钟,Vue 初始化 500ms,这种就属于经典的 FCP 或 LCP 页面初始化慢的情况。

当然还有另一种情况:页面出来了,但是一打开就很卡。虽然页面开始显示,但这种情况其实是主线程被长任务占用了,这就是经典的运行时 Performance。

所以,我们需要区分是“页面出现得晚”,还是“页面已经出现,但主线程的长任务导致了交互卡顿”:

  1. 如果是页面出现得晚:我们重点要看网络、资源和初始化链路。
  2. 如果是长任务导致的交互卡顿:我们需要重点看主线程、长任务以及 JS 的执行。
  3. 分页切换卡

​可能原因​​:

  • 切换分页时整个列表组件销毁重建,而非复用
  • 每次切换都重新请求全量数据,或前端做全量数据的分页(数据量过大)
  • 切换动画导致强制回流(如修改 height、top 等几何属性)
  • 分页组件本身渲染复杂(如带大量按钮、下拉)

​排查工具​:

  • Performance:录制切换动作,观察 Layout / Recalc Style 耗时
  • Vue Devtools → Component Tree:查看切换时组件是否被卸载再挂载
  • Console:打印 mounted 和 unmounted 日志,确认组件生命周期

​解决方案​:

  • 使用虚拟滚动(如 vue-virtual-scroller)只渲染可视区域
  • 分页数据只请求当前页,不做全量加载
  • 动画使用 transform / opacity 避开 Layout
  • 分页控件用 shallowRef 或 v-memo 减少不必要的子组件更新 (只是作为一种优化思路,真实开发中能不用就不用,具体原因下面有写到)

这里特别强调,不建议直接说 keep-alive 。因为我们需要先确认分页切换时,到底发生的是一个组件的更新,还是组件发生了卸载和挂载。千万不能在没有确定生命周期行为之前,就直接使用 keep-alive。

  1. 加载数据卡

​可能原因​​:

  • 请求返回后,前端对大量数据做了复杂转换(排序、分组、深拷贝)且放在 watch 或 computed 中同步执行
  • 数据更新触发了大量 watcher 和 computed 重新求值
  • 没有 loading 状态,用户在等待时界面僵死(因为主线程被占)
  • 请求未做防抖,用户连续操作导致并发请求堆积

​排查工具​:

  • Performance:查看数据返回后的 Long Task,定位是哪段代码占用了主线程
  • Sources → Call Stack:在数据更新的地方打断点,查看调用栈
  • Vue Devtools → Components:观察数据更新后哪些组件重新渲染

当然,这里面试官可能会追问:通过 performance 看到两秒的长任务,怎么确定是 Vue?

此时我们主要可以这样回答:

  1. 通过 performance 定位到主线程的长任务。
  2. 通过 call tree 和 bottom up 来看具体的 JS function。
  3. 如果 function 落到 render reactive effect 或者 patch 等 API 上,再结合 Vue 的 dev tools,就可以定位到具体的组件。

​解决方案​:

  • 数据处理移到 Web Worker 中,主线程只负责赋值
  • 使用 computed 缓存而非 methods 在模板中调用
  • 对大量数据使用 Object.freeze 冻结静态数据,跳过响应式包装 (平时的业务开发用到的少,一般用于库的开发)
  • 加入 loading 骨架屏,避免用户感知空白 (这里很多同学有一个误区:骨架屏只是优化用户体验,而不是优化性能的。如果我们主线程卡顿好几秒,其实放个骨架屏也没有用,因为主线程被 JS 占用,连骨架屏自己都可能无法及时绘制)
  • 使用 debounce 或 AbortController 取消过期请求,避免过期结果回写前端,减少不必要的客户端处理

上面对于数据请求响应后做监听的行为,非常建议把详细的链条说出来:

  1. 当 HTTP 响应之后,我们一步解析得到响应数据,并且修改响应式的 state,进而触发 trigger 变更。
  2. 通过 Vue 的 scheduler 调度,然后组件 update job 进行入队,之后 nextTick 可批量执行。
  3. 页面通过 render 进行渲染,进而生成或者复用虚拟节点。
  4. 然后执行 patch, 操作 DOM 更新。
  5. 浏览器最终进行布局、绘制或者合成。

因此,如果面试官想要更深入地回答的话,其实性能问题还可能存在于这些场景:

  1. 数据处理
  2. 响应式触发
  3. Computed 和 watcher
  4. 组件的 render
  5. 虚拟 DOM 的 diff 和 patch
  6. 浏览器的 layout 和 paint

问题二解答

问题二:如果 A 组件是一个筛选组件,当 A 组件改变了筛选项,导致 B 列表、C 统计图组件的更新突然变卡,请从 Vue 执行的链路来分析整个过程。

我把这个问题的执行链路拆分一下便于思考:

filter.category = 'book'

1. filter.category 被修改
2. Proxy setter / reactive trigger
3. 找到依赖 category 的 effects
4. Vue scheduler 对更新任务进行调度
5. 相关 component update job 被执行
6. B/C 如果确实依赖该状态,则重新执行 render
7. render 产生新的 VNode
8. Vue patch 新旧 VNode
9. DOM 更新
10. 浏览器执行 Style/Layout/Paint/Composite
  1. 现象本质

A 的筛选项(通常是 reactive 对象或 Pinia store 里的 state)发生变化后,B 和 C 组件重新渲染,但渲染耗时明显过长,导致界面卡顿。

  1. Vue 链路中的卡顿根源

如果面试官并没有强调具体的场景,我们可以根据这几种原因依次分析。

着重强调一下关于列表中的 key:当列表只是尾部追加,没有重排序时,Index 作为 key 未必会造成明显的问题;只有在列表发生插入、删除或者排序的时候,Index 作为 key 并不能稳定地表示业务实体的身份,这时候才容易出现错误的复用。

当然,其实如果面试官要追问的话,我们也不能直接说父组件更新就一定会导致整棵子树的重新渲染,因为 Vue 3 内部其实已经做了非常多的优化。

父组件更新之后,Vue 会对其子组件进行必要的更新检查。是否导致子组件真正执行更新,其实还要看子组件依赖的 props 变化以及编译优化后的 patch 信息等,而不能简单地说整个子组件全部会重新渲染。

  1. 排查方法

  • Vue Devtools → Performance 面板:录制筛选项变化过程,查看 B/C 组件的 render 耗时、setup 调用次数、computed 重新求值次数。
  • console.time 打点:在 B/C 的 beforeUpdate 和 updated 钩子中打印耗时,定位哪个组件最慢。
  • 检查 props 引用:确认 A 传给 B/C 的筛选项是否每次都生成新对象(如 ...state 展开),导致子组件认为 props 变了。

这些只是排查的方法,那我们应该如何快速定位呢?我这里把定位做了一些分层,可以供大家参考:

​第一层是数据层。​通过筛选改变进而触发接口请求,返回 10 万条数据,这样就可以去检查我们的数据量以及序列化、反序列化和数据转换等操作。

​第二层是响应式层​。比如说我们的 state 发生了改变,进而触发了 trigger,那么其他的响应式 API(比如 computed、watch 或者组件的副作用)都会执行。

在这种情况下,我们需要思考:

  1. 是否监听了过大的对象?
  2. 是否存在无关的依赖?
  3. computed 是否会频繁地失效?
  4. watch 是否触发了大量的副作用?

​第三层是 Vue 渲染层​。当组件更新时,触发 render 渲染,并且进行虚拟 DOM 对比更新以及 patch。

这种情况下,我们需要思考:

  1. render 是否执行了太多次?
  2. 列表是不是有成千上万个节点,或者存在大量无意义的更新?
  3. props 是否因为引用的变化导致了更新?
  4. key 是否合理?

​最后一个层面是浏览器的渲染层面​。这一点其实主要回答的是“浏览器渲染原理”这道面试题。我们根据渲染原理,比如说 DOM 更新触发 style 的变更,进而触发重排、重绘以及合成层。其中主要涉及以下情况:

  1. 大量的 DOM 强制同步布局;
  2. 复杂的 CSS 动画;
  3. 大面积的绘制。

Chrome DevTools 的 Performance / Rendering / Layers 等工具可以进一步确认是 JS 执行、布局绘制还是合成阶段占用时间。

  1. 解决方案

// 1. 精细化响应式:只监听具体字段
const filter = reactive({ category: '', priceRange: [] })
// B 组件只关心 category
const filteredItems = computed(() => items.filter(i => i.category === filter.category))

// 2. 用 computed 缓存昂贵计算
const sortedList = computed(() => [...rawList].sort(/* ... */))

// 3. 子组件包裹 memo (Vue 3.3+)(了解即可,开发中很少用)
<script setup>
defineOptions({ inheritAttrs: false })
</script>
// 或用 shallowRef + triggerRef 手动控制更新(了解即可,开发中很少用)

// 4. 列表加稳定 key,且不要用 index
<li v-for="item in list" :key="item.id">

// 5. 使用 Pinia getter 缓存派生数据
export const useStore = defineStore('main', () => {
  const expensiveList = computed(() => heavyComputation(items.value))
})

同时这里我们也需要明确,computed 虽然能够缓存高昂计算,但是不是用了 computed 就一定快。computed 的依赖是否稳定,以及依赖变化频率是否合理,才是关键。

还有,官方很明确地强调过,v-memo 应该尽量少使用。只有明确确定存在大量无意义的子树更新时,才会考虑使用 v-memo 跳过特定子树的更新。这是一种非常偏底层的微优化,我们面试时候可以说,但在工作中一定要慎重再慎重地去使用。

话术总结

面对“页面卡顿”,我不会先猜是哪个组件有问题,而是先用 Performance 把卡顿定位到网络、JS 执行、Vue 更新还是浏览器渲染阶段。

如果是 Vue 更新导致的卡顿,我会从响应式数据变更开始追:A 修改筛选条件 → 触发对应响应式依赖 → Vue scheduler 调度更新任务 → 相关组件执行 render → 生成 VNode → patch → DOM 更新 → 浏览器进行 Layout/Paint。

然后分别检查四类问题:
第一,是否有过大的响应式依赖和无意义的 computed/watch;
第二,是否因为 props 引用变化等原因导致无意义组件更新;
第三,render、diff、列表渲染是否过重;第四,DOM 更新后是否产生大量 Layout/Paint。
最后再根据定位结果选择 computed、v-memo、虚拟列表、拆分组件、Web Worker 等具体优化,而不是一上来就堆优化手段。

posted @ 2026-08-22 00:31  秀秀不只会前端  阅读(40)  评论(0)    收藏  举报