⭐ 推荐:社区规则条款 V1.0

为什么我的Vue列表页滚动时会掉帧?从diff与渲染看性能优化

一只冷漠的狐狸
一只冷漠的狐狸 正式会员正式会员认证极客认证极客 钻石卡会员钻石卡会员
发布于 2026-07-19 01:14 ·88 浏览 ·0 回复
内容摘要

Vue 列表页滚动掉帧的根源不在 Vue 本身,而是滚动高频触发响应式更新,叠加虚拟 DOM diff 与浏览器渲染在主线程的开销击穿了 16ms 帧预算;解决思路是避免滚动写入响应式状态或频繁 patch,使用唯一 key、拆分列表项组件、v-memo 控制更新范围,并用虚拟滚动把真实 DOM 节点数从数千降到数十。

列表页滚动掉帧,几乎是每个前端都踩过的坑:数据量不算夸张,接口也很快,但手指一滑,页面就像在放 PPT。更让人困惑的是,打开 Vue Devtools 一看,组件也没更新几次,为什么帧率还是上不去?

答案往往不在“Vue 慢”,而在于滚动这个高频行为,恰好撞上了 Vue 的响应式更新、虚拟 DOM diff 和浏览器渲染流水线三件事。只要其中一环在主线程上耗时过长,16ms 的帧预算就会被击穿。

掉帧的本质:主线程被谁占住了

浏览器要维持 60fps,每帧只有约 16.6ms。滚动时,浏览器需要处理事件、执行 JS、计算样式、布局、绘制、合成。如果 JS 执行时间过长,后面的步骤只能排队,用户看到的就是卡顿。

Vue 的更新是异步批量的,但批量不代表不耗时。当响应式数据变化时,Vue 会重新执行组件渲染函数,生成新的 VNode,再和旧 VNode 做 diff,最后把差异应用到真实 DOM。这一整套流程都在主线程完成。列表越长、组件越复杂,单次更新的成本就越高。

所以,滚动掉帧通常不是“滚动本身”导致的,而是滚动过程中触发了不该触发的更新,或者更新范围远远超过了必要范围。

滚动时,Vue 可能在做哪些无用功

一个很常见的写法是监听滚动,把 scrollTop 存进响应式数据:

const scrollTop = ref(0)
window.addEventListener('scroll', () => {
  scrollTop.value = window.scrollY
})

如果这个 scrollTop 被模板中的多个组件依赖,那么每一次滚动都会触发这些组件重新渲染。滚动事件触发频率极高,一秒可能几十次,Vue 的调度器虽然会合并同一 tick 的更新,但下一帧又来一轮。主线程就这样被反复占用。

更隐蔽的是,有些列表项内部用了 IntersectionObserver 或 getBoundingClientRect 来判断可见性,回调里又修改响应式状态。滚动时,观察器频繁触发,状态频繁更新,组件频繁 patch,掉帧几乎是必然的。

diff 的成本:key 与更新范围

Vue 的 diff 算法在同层比较,复杂度与节点数量相关。Vue 2 使用双端比较,Vue 3 在编译时标记动态节点,并借助最长递增子序列减少移动操作。但无论哪个版本,diff 都不是免费的。

如果列表使用 index 作为 key,在插入、删除、排序时,Vue 会按位置复用节点,导致大量不必要的 DOM 更新,甚至状态错乱。用唯一 id 作为 key,能让 diff 更准确地识别节点,减少真实 DOM 操作。

另一个容易被忽略的点是组件粒度。如果整个列表是一个大组件,任何一条数据变化都会导致整个列表重新渲染。把列表项拆成独立组件,让响应式依赖落在更小的范围内,diff 的规模会显著下降。Vue 3 的 v-memo 也能缓存子树,在依赖不变时跳过更新。

渲染的成本:DOM 数量与样式复杂度

即使 diff 很快,真实 DOM 的创建、插入、样式计算和布局也可能很贵。一个列表渲染上千个节点,每个节点又有阴影、圆角、渐变、复杂选择器,浏览器在滚动时就需要不断合成和绘制。

这时候,虚拟滚动是最直接的手段。只渲染视口内可见的几十个节点,滚动时复用 DOM,把节点数量从几千降到几十,diff 和渲染的开销都会断崖式下降。vue-virtual-scroller、@vueuse/core 的 useVirtualList 都是成熟方案。

如果列表项高度不固定,虚拟滚动会复杂一些,但依然比全量渲染划算。还可以配合 content-visibility: auto,让浏览器跳过屏幕外元素的渲染工作;用 contain: layout paint 限制布局和绘制范围;用 will-change: transform 把滚动动画交给合成层。

几个真正有效的优化方向

第一,先测量再优化。用 Chrome Performance 面板录制一段滚动,看 Long Task 出现在哪里,是 JS 执行、样式计算还是布局。不要凭感觉猜。

第二,减少滚动过程中的响应式写入。滚动回调里如果必须更新状态,用 requestAnimationFrame 节流,或者干脆用 CSS 变量、transform 直接操作 DOM,绕开 Vue 的更新流程。

第三,冻结不需要响应式的大数据。用 Object.freeze、shallowRef、markRaw 告诉 Vue “这些数据不会变”,省掉 Proxy 或 defineProperty 的递归转换开销。

第四,合理拆分组件,善用 computed 缓存派生数据,避免在模板里写复杂表达式和函数调用。Vue 3 的 v-memo、v-once 在合适场景下也能省下大量 diff。

第五,别忘了 CSS。contain、content-visibility、will-change 这些属性不是玄学,它们能直接影响浏览器的渲染决策。

小结

Vue 列表页滚动掉帧,表面看是滚动问题,根子通常在响应式更新范围过大、diff 规模失控、真实 DOM 数量过多,以及滚动事件里做了太多同步工作。优化的核心思路只有一句话:让每一次滚动帧里,主线程要做的事情尽可能少。先定位瓶颈,再选择虚拟滚动、组件拆分、数据冻结或 CSS 隔离,才能把帧率真正拉回来。

本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-204.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 0

还没有回复,来抢沙发~