固定高度的长列表怎么优化滚动性能?

诸葛子瑄 阅读 12

我有个聊天记录列表,每条消息高度固定为60px,但数据一多(比如上千条)页面就卡得不行。试过用虚拟滚动,但自己写的逻辑好像有问题,滚动时经常白屏或者错位。

这是我现在计算可视区域渲染项的代码:

const ITEM_HEIGHT = 60;
const containerHeight = 500;
const startIndex = Math.floor(scrollTop / ITEM_HEIGHT);
const endIndex = startIndex + Math.ceil(containerHeight / ITEM_HEIGHT) + 2;
const visibleItems = allItems.slice(startIndex, endIndex);

为啥还是卡?是不是哪里漏了?

我来解答 赞 4 收藏
二维码
手机扫码查看
1 条解答
Newb.萍萍
你这代码看着挺简单,但问题出在两个地方:一是没处理滚动容器本身的高度动态变化,二是没做真正的虚拟滚动优化——只做了“按需显示”,没做“按需渲染”。

先说问题:你用 [slice](file:///Users/xxx/project/node_modules/lodash/slice.js#L21-L27) 直接从数组里截取数据,前端还是得把几百条 DOM 全挂上去,只是视觉上“看起来只显示一部分”,浏览器照样要 layout 和 paint 几百个节点,当然卡。

虚拟滚动的核心不是“算范围”,而是“只渲染可视区域内的 DOM,其他用占位空间撑起高度”。

你现在的逻辑只解决了“哪些数据该显示”,但没解决“怎么让滚动条还能正常滚动”。

正确的做法是:

1. 整个列表容器设为相对定位,高度固定(比如 [500px](file:///Users/xxx/project/node_modules/normalize.css/normalize.css))
2. 在容器里放一个“占位容器”,高度是 [allItems.length * ITEM_HEIGHT](file:///Users/xxx/project/node_modules/lodash/number.js#L13-L15)
3. 你的实际消息列表不要直接挂容器里,而是挂到这个占位容器里,用绝对定位定位到对应位置(top = index * 60)

举个简化版结构:









重点是 [visibleItems](file:///Users/xxx/project/node_modules/lodash/clone.js#L31-L35) 里只放真正能看到的那几十条,其他都别渲染。

另外你计算 [endIndex](file:///Users/xxx/project/node_modules/lodash/number.js#L18-L20) 的时候加了 [2](file:///Users/xxx/project/node_modules/lodash/number.js#L16-L18),这没问题,是为了预渲染一点点避免白边,但一定要配合 [scroll](file:///Users/xxx/project/node_modules/lodash/array.js#L17-L23) 事件节流,不然每次 [scroll](file:///Users/xxx/project/node_modules/lodash/array.js#L17-L23) 都重新算一遍 [startIndex](file:///Users/xxx/project/node_modules/lodash/number.js#L11-L13)、[endIndex](file:///Users/xxx/project/node_modules/lodash/number.js#L18-L20)、[visibleItems](file:///Users/xxx/project/node_modules/lodash/clone.js#L31-L35),再触发组件重渲染,性能肯定崩。

加个 [debounce](file:///Users/xxx/project/node_modules/lodash/debounce.js#L11-L13) 或者 [requestAnimationFrame](file:///Users/xxx/project/node_modules/lodash/lang.js#L12-L14):

let ticking = false
function onScroll() {
if (!ticking) {
window.requestAnimationFrame(() => {
const scrollTop = container.scrollTop
const startIndex = Math.floor(scrollTop / ITEM_HEIGHT)
const endIndex = startIndex + Math.ceil(containerHeight / ITEM_HEIGHT) + 2
visibleItems.value = allItems.slice(startIndex, endIndex)
ticking = false
})
ticking = true
}
}

container.addEventListener('scroll', onScroll)

最后说句扎心的:聊天记录这种场景,如果真要支持上千条甚至上万条,别光靠前端扛,后端得分页加载,数据库层面也得加索引和分表,不然你前端再怎么优化,接口一查几万条数据回来,光序列化都够呛。

真要干到十万级记录,建议用懒加载 + 滚动到底部自动追加新数据,别一股脑塞上千条——前端再牛也救不了后端没做分页。
点赞 2
2026-02-27 18:30