Perf: Support declarative _itemHeight in VirtualScroll for zero-measurement setup

This commit is contained in:
AI Engineer 2026-05-22 20:01:33 +08:00
parent f5f57638df
commit 01e0067c43

View File

@ -1,12 +1,14 @@
import { Component, NewState, Util, Hash } from '@web/state'
export const VirtualScroll = () => {
export const VirtualScroll = (options = {}) => {
const itemHeights = new Map()
const groupHeights = new Map()
let groupItemCount = 1
const avg = Util.newAvg()
let padTop = 0, rowGap = 0, topMargin = 0, itemMarginTop = null, itemMarginBottom = null, listInited = false
const providedItemHeight = options.itemHeight || null;
return {
reset: (list, container) => {
listInited = false; itemHeights.clear(); groupHeights.clear(); avg.clear(); topMargin = 0; itemMarginTop = null; itemMarginBottom = null;
@ -14,12 +16,33 @@ export const VirtualScroll = () => {
const size = list.length; groupItemCount = Math.ceil(Math.sqrt(size)) || 10;
const style = window.getComputedStyle(container);
padTop = parseFloat(style.paddingTop) || 0; rowGap = parseFloat(style.rowGap) || 0;
return list.slice(0, Math.min(30, size));
// Optimization: Give a reasonably large initial buffer instead of just 30 to prevent the
// "first scroll unresponsiveness" where the user scrolls past the small initial slice
// before the init() callback has time to map all heights.
const visibleCount = Math.max(10, Math.ceil((container.clientHeight || 100) / (providedItemHeight || 32)));
return list.slice(0, Math.min(visibleCount * 3, size));
},
init: (list, refreshCallback) => {
if (listInited) return;
const size = list.length, defaultHeight = avg.get() || 32;
for (let i = 0; i < size; i++) if (!itemHeights.has(i)) itemHeights.set(i, defaultHeight);
const size = list.length;
let defaultHeight = providedItemHeight || avg.get() || 32;
// Optimization: If the first item declares a fixed height, use it as the global baseline.
// This completely bypasses the need for DOM measurement (node.offsetHeight) in fixed-height scenarios.
if (size > 0 && typeof list[0] === 'object' && list[0] !== null && list[0]._itemHeight) {
defaultHeight = list[0]._itemHeight;
}
avg.add(defaultHeight);
if (itemMarginTop === null) { itemMarginTop = 0; itemMarginBottom = 0; }
for (let i = 0; i < size; i++) {
if (!itemHeights.has(i)) {
// Fallback to individual item height if specified, otherwise global default
const ih = (typeof list[i] === 'object' && list[i] !== null && list[i]._itemHeight) ? list[i]._itemHeight : defaultHeight;
itemHeights.set(i, ih);
}
}
for (let i = 0; i < size; i += groupItemCount) groupHeights.set(i, Math.min(groupItemCount, size - i) * defaultHeight);
listInited = true; refreshCallback();
},