diff --git a/src/list.js b/src/list.js index 3ebb622..78f3aa7 100644 --- a/src/list.js +++ b/src/list.js @@ -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(); },