@humanspeak/svelte-virtual-list 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -37
- package/dist/SvelteVirtualList.svelte +208 -47
- package/dist/SvelteVirtualList.svelte.d.ts +6 -1
- package/dist/index.d.ts +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/utils/raf.d.ts +8 -0
- package/dist/utils/raf.js +22 -0
- package/dist/utils/virtualList.d.ts +61 -0
- package/dist/utils/virtualList.js +98 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,12 +12,57 @@
|
|
|
12
12
|
|
|
13
13
|
A virtual list component for Svelte applications. Built for Svelte 5 with TypeScript support.
|
|
14
14
|
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- Efficient rendering of large lists with dynamic heights
|
|
18
|
+
- Bi-directional scrolling (top-to-bottom and bottom-to-top)
|
|
19
|
+
- Automatic resize handling and content updates
|
|
20
|
+
- TypeScript support with full type safety
|
|
21
|
+
- SSR compatible with hydration support
|
|
22
|
+
- Svelte 5 runes and snippets support
|
|
23
|
+
- Customizable styling with class props
|
|
24
|
+
- Debug mode for development
|
|
25
|
+
- Smooth scrolling with configurable buffer zones
|
|
26
|
+
|
|
15
27
|
## Installation
|
|
16
28
|
|
|
17
29
|
```bash
|
|
18
30
|
npm install @humanspeak/svelte-virtual-list
|
|
19
31
|
```
|
|
20
32
|
|
|
33
|
+
## Basic Usage
|
|
34
|
+
|
|
35
|
+
```svelte
|
|
36
|
+
<script lang="ts">
|
|
37
|
+
import SvelteVirtualList from '@humanspeak/svelte-virtual-list'
|
|
38
|
+
|
|
39
|
+
const items = Array.from({ length: 1000 }, (_, i) => ({
|
|
40
|
+
id: i,
|
|
41
|
+
text: `Item ${i}`
|
|
42
|
+
}))
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<SvelteVirtualList {items}>
|
|
46
|
+
{#snippet renderItem(item)}
|
|
47
|
+
<div>{item.text}</div>
|
|
48
|
+
{/snippet}
|
|
49
|
+
</SvelteVirtualList>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Props
|
|
53
|
+
|
|
54
|
+
| Prop | Type | Default | Description |
|
|
55
|
+
| ------------------- | -------------------------------- | --------------- | ------------------------------------------- |
|
|
56
|
+
| `items` | `T[]` | Required | Array of items to render |
|
|
57
|
+
| `defaultItemHeight` | `number` | `40` | Initial height for items before measurement |
|
|
58
|
+
| `mode` | `'topToBottom' \| 'bottomToTop'` | `'topToBottom'` | Scroll direction |
|
|
59
|
+
| `bufferSize` | `number` | `20` | Number of items to render outside viewport |
|
|
60
|
+
| `debug` | `boolean` | `false` | Enable debug logging and visualizations |
|
|
61
|
+
| `containerClass` | `string` | `''` | Class for outer container |
|
|
62
|
+
| `viewportClass` | `string` | `''` | Class for scrollable viewport |
|
|
63
|
+
| `contentClass` | `string` | `''` | Class for content wrapper |
|
|
64
|
+
| `itemsClass` | `string` | `''` | Class for items container |
|
|
65
|
+
|
|
21
66
|
## Dependencies
|
|
22
67
|
|
|
23
68
|
- [esm-env](https://github.com/benmccann/esm-env) - svelte5 suggested environment detecting
|
|
@@ -65,43 +110,6 @@ npm install @humanspeak/svelte-virtual-list
|
|
|
65
110
|
</div>
|
|
66
111
|
```
|
|
67
112
|
|
|
68
|
-
## Props
|
|
69
|
-
|
|
70
|
-
The VirtualList component accepts the following props:
|
|
71
|
-
|
|
72
|
-
- `items` - Array of items to render
|
|
73
|
-
- `defaultItemHeight` - Initial height of each item in pixels (optional, defaults to 40)
|
|
74
|
-
- `mode` - Scroll direction ('topToBottom' or 'bottomToTop')
|
|
75
|
-
- `bufferSize` - Number of items to render outside the visible area (optional, defaults to 20)
|
|
76
|
-
- `debug` - Enable debug mode (optional)
|
|
77
|
-
- `containerClass` - Custom class for container element (optional)
|
|
78
|
-
- `viewportClass` - Custom class for viewport element (optional)
|
|
79
|
-
- `contentClass` - Custom class for content element (optional)
|
|
80
|
-
- `itemsClass` - Custom class for items wrapper (optional)
|
|
81
|
-
- `renderItem` - Snippet function to render each item
|
|
82
|
-
|
|
83
|
-
Note: The component will automatically calculate the average item height based on rendered items, using `defaultItemHeight` only as an initial value until real measurements are available.
|
|
84
|
-
|
|
85
|
-
### Buffer Size
|
|
86
|
-
|
|
87
|
-
The `bufferSize` prop determines how many additional items are rendered outside the visible area. A larger buffer:
|
|
88
|
-
|
|
89
|
-
- Reduces the chance of seeing blank spaces during fast scrolling
|
|
90
|
-
- Provides smoother scrolling experience
|
|
91
|
-
- Increases memory usage (as more items are rendered)
|
|
92
|
-
|
|
93
|
-
Default value is 20 items, which provides a good balance between performance and smoothness.
|
|
94
|
-
|
|
95
|
-
## Features
|
|
96
|
-
|
|
97
|
-
- Efficient rendering of large lists
|
|
98
|
-
- TypeScript support
|
|
99
|
-
- Customizable styling
|
|
100
|
-
- Debug mode for development
|
|
101
|
-
- Smooth scrolling with buffer zones
|
|
102
|
-
- SSR compatible
|
|
103
|
-
- Svelte 5 runes support
|
|
104
|
-
|
|
105
113
|
## Development
|
|
106
114
|
|
|
107
115
|
```bash
|
|
@@ -188,6 +196,52 @@ The component automatically handles:
|
|
|
188
196
|
|
|
189
197
|
No manual intervention is needed when item contents or dimensions change.
|
|
190
198
|
|
|
199
|
+
## Advanced Usage
|
|
200
|
+
|
|
201
|
+
### Chat Application Example
|
|
202
|
+
|
|
203
|
+
```svelte
|
|
204
|
+
<script lang="ts">
|
|
205
|
+
import SvelteVirtualList from '@humanspeak/svelte-virtual-list'
|
|
206
|
+
|
|
207
|
+
type Message = {
|
|
208
|
+
id: number
|
|
209
|
+
text: string
|
|
210
|
+
timestamp: Date
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const messages: Message[] = Array.from({ length: 100 }, (_, i) => ({
|
|
214
|
+
id: i,
|
|
215
|
+
text: `Message ${i}`,
|
|
216
|
+
timestamp: new Date()
|
|
217
|
+
}))
|
|
218
|
+
</script>
|
|
219
|
+
|
|
220
|
+
<SvelteVirtualList
|
|
221
|
+
items={messages}
|
|
222
|
+
mode="bottomToTop"
|
|
223
|
+
containerClass="h-screen"
|
|
224
|
+
viewportClass="bg-gray-100"
|
|
225
|
+
>
|
|
226
|
+
{#snippet renderItem(message)}
|
|
227
|
+
<div class="p-4 rounded-lg shadow-sm">
|
|
228
|
+
<p>{message.text}</p>
|
|
229
|
+
<span class="text-sm text-gray-500">
|
|
230
|
+
{message.timestamp.toLocaleString()}
|
|
231
|
+
</span>
|
|
232
|
+
</div>
|
|
233
|
+
{/snippet}
|
|
234
|
+
</SvelteVirtualList>
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## Performance Considerations
|
|
238
|
+
|
|
239
|
+
- The `bufferSize` prop affects memory usage and scroll smoothness
|
|
240
|
+
- Items are measured and cached for optimal performance
|
|
241
|
+
- Dynamic height calculations happen automatically
|
|
242
|
+
- Resize observers handle container/content changes
|
|
243
|
+
- Virtual DOM updates are batched for efficiency
|
|
244
|
+
|
|
191
245
|
## Related
|
|
192
246
|
|
|
193
247
|
- [Svelte](https://svelte.dev) - JavaScript front-end framework
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
@component
|
|
3
3
|
A high-performance virtualized list component that efficiently renders large datasets
|
|
4
|
-
by only mounting DOM nodes for visible items and a small buffer.
|
|
4
|
+
by only mounting DOM nodes for visible items and a small buffer. Optimized for handling
|
|
5
|
+
lists of 10k+ items through chunked processing and progressive initialization.
|
|
5
6
|
|
|
6
7
|
Props:
|
|
7
8
|
- `items` - Array of items to render
|
|
@@ -34,6 +35,10 @@
|
|
|
34
35
|
- Configurable buffer size
|
|
35
36
|
- Debug mode
|
|
36
37
|
- Custom styling
|
|
38
|
+
- Progressive initialization for large datasets
|
|
39
|
+
- Memory-optimized for 10k+ items
|
|
40
|
+
- Chunked processing for smooth performance
|
|
41
|
+
- Progress tracking during initialization
|
|
37
42
|
-->
|
|
38
43
|
|
|
39
44
|
<script lang="ts">
|
|
@@ -66,18 +71,28 @@
|
|
|
66
71
|
* - Implemented proper cleanup on component destruction
|
|
67
72
|
* - Added debug mode for development assistance
|
|
68
73
|
*
|
|
74
|
+
* 6. Large Dataset Optimizations
|
|
75
|
+
* - Implemented chunked processing for 10k+ items
|
|
76
|
+
* - Added progressive initialization system
|
|
77
|
+
* - Deferred height calculations for better initial load
|
|
78
|
+
* - Optimized memory usage for large lists
|
|
79
|
+
* - Added progress tracking for initialization
|
|
80
|
+
*
|
|
69
81
|
* Technical Challenges Solved:
|
|
70
82
|
* - Bottom-to-top scrolling in flexbox layouts
|
|
71
83
|
* - Dynamic height calculations without layout thrashing
|
|
72
84
|
* - Smooth scrolling on various devices
|
|
73
85
|
* - Memory management for large lists
|
|
74
86
|
* - Browser compatibility issues
|
|
87
|
+
* - Performance optimization for 10k+ items
|
|
88
|
+
* - Progressive initialization for large datasets
|
|
75
89
|
*
|
|
76
90
|
* Current Architecture:
|
|
77
91
|
* - Four-layer DOM structure for optimal performance
|
|
78
92
|
* - State management using Svelte 5's $state
|
|
79
93
|
* - Reactive height and scroll calculations
|
|
80
94
|
* - Configurable buffer zones for smooth scrolling
|
|
95
|
+
* - Chunked processing system for large datasets
|
|
81
96
|
*/
|
|
82
97
|
|
|
83
98
|
import { onMount } from 'svelte'
|
|
@@ -87,42 +102,98 @@
|
|
|
87
102
|
calculateScrollPosition,
|
|
88
103
|
calculateVisibleRange,
|
|
89
104
|
calculateTransformY,
|
|
90
|
-
updateHeightAndScroll as utilsUpdateHeightAndScroll
|
|
105
|
+
updateHeightAndScroll as utilsUpdateHeightAndScroll,
|
|
106
|
+
calculateAverageHeight,
|
|
107
|
+
processChunked
|
|
91
108
|
} from './utils/virtualList.js'
|
|
109
|
+
import { rafSchedule } from './utils/raf.js'
|
|
92
110
|
|
|
93
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Core configuration props with default values
|
|
113
|
+
* @type {SvelteVirtualListProps}
|
|
114
|
+
*/
|
|
94
115
|
const {
|
|
95
|
-
items = [],
|
|
96
|
-
defaultEstimatedItemHeight = 40,
|
|
97
|
-
debug = false,
|
|
98
|
-
renderItem,
|
|
99
|
-
containerClass,
|
|
100
|
-
viewportClass,
|
|
101
|
-
contentClass,
|
|
102
|
-
itemsClass,
|
|
103
|
-
debugFunction,
|
|
104
|
-
mode = 'topToBottom',
|
|
105
|
-
bufferSize = 20
|
|
116
|
+
items = [], // Array of items to be rendered in the virtual list
|
|
117
|
+
defaultEstimatedItemHeight = 40, // Initial height estimate for items before measurement
|
|
118
|
+
debug = false, // Enable debug logging
|
|
119
|
+
renderItem, // Function to render each item
|
|
120
|
+
containerClass, // Custom class for the container element
|
|
121
|
+
viewportClass, // Custom class for the viewport element
|
|
122
|
+
contentClass, // Custom class for the content wrapper
|
|
123
|
+
itemsClass, // Custom class for the items wrapper
|
|
124
|
+
debugFunction, // Custom debug logging function
|
|
125
|
+
mode = 'topToBottom', // Scroll direction mode
|
|
126
|
+
bufferSize = 20 // Number of items to render outside visible area
|
|
106
127
|
}: SvelteVirtualListProps = $props()
|
|
107
128
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
129
|
+
/**
|
|
130
|
+
* DOM References and Core State
|
|
131
|
+
*/
|
|
132
|
+
let containerElement: HTMLElement // Reference to the main container element
|
|
133
|
+
let viewportElement: HTMLElement // Reference to the scrollable viewport element
|
|
134
|
+
const itemElements = $state<HTMLElement[]>([]) // Array of rendered item element references
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Scroll and Height Management
|
|
138
|
+
*/
|
|
112
139
|
let scrollTop = $state(0) // Current scroll position
|
|
113
|
-
let initialized = $state(false) // Tracks if initial setup is complete
|
|
114
140
|
let height = $state(0) // Container height
|
|
115
141
|
let calculatedItemHeight = $state(defaultEstimatedItemHeight) // Current average item height
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* State Flags and Control
|
|
145
|
+
*/
|
|
146
|
+
let initialized = $state(false) // Tracks if initial setup is complete
|
|
116
147
|
let isCalculatingHeight = $state(false) // Prevents concurrent height calculations
|
|
117
|
-
let
|
|
118
|
-
let
|
|
119
|
-
let resizeObserver: ResizeObserver | null = null
|
|
148
|
+
let isScrolling = $state(false) // Tracks active scrolling state
|
|
149
|
+
let lastMeasuredIndex = $state(-1) // Index of last measured item
|
|
120
150
|
|
|
121
151
|
/**
|
|
122
|
-
*
|
|
123
|
-
* Uses debouncing to prevent excessive calculations
|
|
152
|
+
* Timers and Observers
|
|
124
153
|
*/
|
|
125
|
-
|
|
154
|
+
let heightUpdateTimeout: ReturnType<typeof setTimeout> | null = null // Debounce timer for height updates
|
|
155
|
+
let resizeObserver: ResizeObserver | null = null // Watches for container size changes
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Performance Optimization State
|
|
159
|
+
*/
|
|
160
|
+
let heightCache = $state<Record<number, number>>({}) // Cache of measured item heights
|
|
161
|
+
const chunkSize = $state(50) // Number of items to process in each chunk
|
|
162
|
+
let processedItems = $state(0) // Number of items processed during initialization
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Calculates and updates the average height of visible items with debouncing.
|
|
166
|
+
*
|
|
167
|
+
* This function optimizes performance by:
|
|
168
|
+
* - Debouncing calculations to prevent excessive DOM reads
|
|
169
|
+
* - Caching item heights to minimize recalculations
|
|
170
|
+
* - Only updating when significant changes are detected
|
|
171
|
+
*
|
|
172
|
+
* Implementation details:
|
|
173
|
+
* - Uses a 200ms debounce timeout
|
|
174
|
+
* - Tracks calculation state to prevent concurrent updates
|
|
175
|
+
* - Caches heights in heightCache for reuse
|
|
176
|
+
* - Only updates if height difference > 1px
|
|
177
|
+
*
|
|
178
|
+
* State interactions:
|
|
179
|
+
* - Updates calculatedItemHeight
|
|
180
|
+
* - Updates lastMeasuredIndex
|
|
181
|
+
* - Modifies heightCache
|
|
182
|
+
* - Uses/sets isCalculatingHeight flag
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* ```typescript
|
|
186
|
+
* // Automatically called when items are rendered
|
|
187
|
+
* $effect(() => {
|
|
188
|
+
* if (BROWSER && itemElements.length > 0 && !isCalculatingHeight) {
|
|
189
|
+
* calculateAverageHeightDebounced()
|
|
190
|
+
* }
|
|
191
|
+
* })
|
|
192
|
+
* ```
|
|
193
|
+
*
|
|
194
|
+
* @returns {void}
|
|
195
|
+
*/
|
|
196
|
+
const calculateAverageHeightDebounced = () => {
|
|
126
197
|
if (!BROWSER || isCalculatingHeight || heightUpdateTimeout) return
|
|
127
198
|
isCalculatingHeight = true
|
|
128
199
|
|
|
@@ -134,35 +205,32 @@
|
|
|
134
205
|
const visibleRange = visibleItems()
|
|
135
206
|
const currentIndex = visibleRange.start
|
|
136
207
|
|
|
137
|
-
// Only recalculate if we're looking at different items
|
|
138
208
|
if (currentIndex !== lastMeasuredIndex) {
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
lastMeasuredIndex = currentIndex
|
|
153
|
-
}
|
|
209
|
+
const { newHeight, newLastMeasuredIndex, updatedHeightCache } =
|
|
210
|
+
calculateAverageHeight(
|
|
211
|
+
itemElements,
|
|
212
|
+
visibleRange,
|
|
213
|
+
heightCache,
|
|
214
|
+
lastMeasuredIndex,
|
|
215
|
+
calculatedItemHeight
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
if (Math.abs(newHeight - calculatedItemHeight) > 1) {
|
|
219
|
+
calculatedItemHeight = newHeight
|
|
220
|
+
lastMeasuredIndex = newLastMeasuredIndex
|
|
221
|
+
heightCache = updatedHeightCache
|
|
154
222
|
}
|
|
155
223
|
}
|
|
156
224
|
|
|
157
225
|
isCalculatingHeight = false
|
|
158
226
|
heightUpdateTimeout = null
|
|
159
|
-
}, 200)
|
|
227
|
+
}, 200)
|
|
160
228
|
}
|
|
161
229
|
|
|
162
230
|
// Trigger height calculation when items are rendered
|
|
163
231
|
$effect(() => {
|
|
164
232
|
if (BROWSER && itemElements.length > 0 && !isCalculatingHeight) {
|
|
165
|
-
|
|
233
|
+
calculateAverageHeightDebounced()
|
|
166
234
|
}
|
|
167
235
|
})
|
|
168
236
|
|
|
@@ -196,7 +264,25 @@
|
|
|
196
264
|
}
|
|
197
265
|
})
|
|
198
266
|
|
|
199
|
-
|
|
267
|
+
/**
|
|
268
|
+
* Calculates the range of items that should be rendered based on current scroll position.
|
|
269
|
+
*
|
|
270
|
+
* This derived calculation determines which items should be visible in the viewport,
|
|
271
|
+
* including the buffer zone. It takes into account:
|
|
272
|
+
* - Current scroll position
|
|
273
|
+
* - Viewport height
|
|
274
|
+
* - Item height estimates
|
|
275
|
+
* - Buffer size
|
|
276
|
+
* - Scroll direction mode
|
|
277
|
+
*
|
|
278
|
+
* @example
|
|
279
|
+
* ```typescript
|
|
280
|
+
* const range = visibleItems()
|
|
281
|
+
* console.log(`Rendering items from ${range.start} to ${range.end}`)
|
|
282
|
+
* ```
|
|
283
|
+
*
|
|
284
|
+
* @returns {{ start: number, end: number }} Object containing start and end indices of visible items
|
|
285
|
+
*/
|
|
200
286
|
const visibleItems = $derived(() => {
|
|
201
287
|
if (!items.length) return { start: 0, end: 0 }
|
|
202
288
|
const viewportHeight = height || 0
|
|
@@ -211,10 +297,37 @@
|
|
|
211
297
|
)
|
|
212
298
|
})
|
|
213
299
|
|
|
214
|
-
|
|
300
|
+
/**
|
|
301
|
+
* Handles scroll events in the viewport using requestAnimationFrame for performance.
|
|
302
|
+
*
|
|
303
|
+
* This function debounces scroll events and updates the scrollTop state only when
|
|
304
|
+
* necessary to prevent excessive re-renders. It uses RAF scheduling to ensure
|
|
305
|
+
* smooth scrolling performance.
|
|
306
|
+
*
|
|
307
|
+
* Implementation details:
|
|
308
|
+
* - Uses isScrolling flag to prevent multiple RAF calls
|
|
309
|
+
* - Updates scrollTop state only when scrolling has settled
|
|
310
|
+
* - Browser-only functionality
|
|
311
|
+
*
|
|
312
|
+
* @example
|
|
313
|
+
* ```svelte
|
|
314
|
+
* <div onscroll={handleScroll}>
|
|
315
|
+
* <!-- scrollable content -->
|
|
316
|
+
* </div>
|
|
317
|
+
* ```
|
|
318
|
+
*
|
|
319
|
+
* @returns {void}
|
|
320
|
+
*/
|
|
215
321
|
const handleScroll = () => {
|
|
216
322
|
if (!BROWSER || !viewportElement) return
|
|
217
|
-
|
|
323
|
+
|
|
324
|
+
if (!isScrolling) {
|
|
325
|
+
isScrolling = true
|
|
326
|
+
rafSchedule(() => {
|
|
327
|
+
scrollTop = viewportElement.scrollTop
|
|
328
|
+
isScrolling = false
|
|
329
|
+
})
|
|
330
|
+
}
|
|
218
331
|
}
|
|
219
332
|
|
|
220
333
|
/**
|
|
@@ -290,6 +403,53 @@
|
|
|
290
403
|
)
|
|
291
404
|
}
|
|
292
405
|
|
|
406
|
+
/**
|
|
407
|
+
* Initializes large datasets in chunks to prevent UI blocking.
|
|
408
|
+
*
|
|
409
|
+
* This function processes items in smaller chunks using setTimeout to yield
|
|
410
|
+
* to the main thread, allowing other UI operations to remain responsive.
|
|
411
|
+
* Progress is tracked and reported through the processedItems state.
|
|
412
|
+
*
|
|
413
|
+
* For datasets larger than 1000 items, this method is automatically used
|
|
414
|
+
* instead of immediate initialization. The chunk size is controlled by the
|
|
415
|
+
* component's chunkSize state (default: 50).
|
|
416
|
+
*
|
|
417
|
+
* @async
|
|
418
|
+
* @example
|
|
419
|
+
* ```typescript
|
|
420
|
+
* // Component initialization
|
|
421
|
+
* $effect(() => {
|
|
422
|
+
* if (BROWSER && items.length > 1000) {
|
|
423
|
+
* initializeChunked()
|
|
424
|
+
* } else {
|
|
425
|
+
* initialized = true
|
|
426
|
+
* }
|
|
427
|
+
* })
|
|
428
|
+
* ```
|
|
429
|
+
*
|
|
430
|
+
* @throws {Error} If processChunked fails to complete initialization
|
|
431
|
+
* @returns {Promise<void>} Resolves when all chunks have been processed
|
|
432
|
+
*/
|
|
433
|
+
const initializeChunked = async () => {
|
|
434
|
+
if (!items.length) return
|
|
435
|
+
|
|
436
|
+
await processChunked(
|
|
437
|
+
items,
|
|
438
|
+
chunkSize,
|
|
439
|
+
(processed) => (processedItems = processed),
|
|
440
|
+
() => (initialized = true)
|
|
441
|
+
)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Modify the mount effect to use chunked initialization
|
|
445
|
+
$effect(() => {
|
|
446
|
+
if (BROWSER && items.length > 1000) {
|
|
447
|
+
initializeChunked()
|
|
448
|
+
} else {
|
|
449
|
+
initialized = true
|
|
450
|
+
}
|
|
451
|
+
})
|
|
452
|
+
|
|
293
453
|
// Setup and cleanup
|
|
294
454
|
onMount(() => {
|
|
295
455
|
if (BROWSER) {
|
|
@@ -364,7 +524,8 @@
|
|
|
364
524
|
visibleItemsCount: visibleItems().end - visibleItems().start,
|
|
365
525
|
startIndex: visibleItems().start,
|
|
366
526
|
endIndex: visibleItems().end,
|
|
367
|
-
totalItems: items.length
|
|
527
|
+
totalItems: items.length,
|
|
528
|
+
processedItems
|
|
368
529
|
}}
|
|
369
530
|
{debugFunction
|
|
370
531
|
? debugFunction(debugInfo)
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { SvelteVirtualListProps } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* A high-performance virtualized list component that efficiently renders large datasets
|
|
4
|
-
* by only mounting DOM nodes for visible items and a small buffer.
|
|
4
|
+
* by only mounting DOM nodes for visible items and a small buffer. Optimized for handling
|
|
5
|
+
* lists of 10k+ items through chunked processing and progressive initialization.
|
|
5
6
|
*
|
|
6
7
|
* Props:
|
|
7
8
|
* - `items` - Array of items to render
|
|
@@ -34,6 +35,10 @@ import type { SvelteVirtualListProps } from './types.js';
|
|
|
34
35
|
* - Configurable buffer size
|
|
35
36
|
* - Debug mode
|
|
36
37
|
* - Custom styling
|
|
38
|
+
* - Progressive initialization for large datasets
|
|
39
|
+
* - Memory-optimized for 10k+ items
|
|
40
|
+
* - Chunked processing for smooth performance
|
|
41
|
+
* - Progress tracking during initialization
|
|
37
42
|
*/
|
|
38
43
|
declare const SvelteVirtualList: import("svelte").Component<SvelteVirtualListProps, {}, "">;
|
|
39
44
|
type SvelteVirtualList = ReturnType<typeof SvelteVirtualList>;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import SvelteVirtualList from './SvelteVirtualList.svelte';
|
|
2
2
|
import type { SvelteVirtualListDebugInfo, SvelteVirtualListMode, SvelteVirtualListProps } from './types.js';
|
|
3
3
|
export default SvelteVirtualList;
|
|
4
|
-
export type { SvelteVirtualListDebugInfo
|
|
4
|
+
export type { SvelteVirtualListDebugInfo, SvelteVirtualListMode, SvelteVirtualListProps };
|
package/dist/types.d.ts
CHANGED
|
@@ -46,10 +46,12 @@ export type SvelteVirtualListProps = {
|
|
|
46
46
|
* @property {number} startIndex - Index of the first rendered item in the viewport.
|
|
47
47
|
* @property {number} totalItems - Total number of items in the list.
|
|
48
48
|
* @property {number} visibleItemsCount - Number of items currently visible in the viewport.
|
|
49
|
+
* @property {number} processedItems - Number of items processed in the viewport.
|
|
49
50
|
*/
|
|
50
51
|
export type SvelteVirtualListDebugInfo = {
|
|
51
52
|
endIndex: number;
|
|
52
53
|
startIndex: number;
|
|
53
54
|
totalItems: number;
|
|
54
55
|
visibleItemsCount: number;
|
|
56
|
+
processedItems: number;
|
|
55
57
|
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schedules a function to be executed on the next animation frame.
|
|
3
|
+
* If a function is already scheduled, the new function will replace it.
|
|
4
|
+
* This helps prevent multiple RAF calls and ensures smooth animations.
|
|
5
|
+
*
|
|
6
|
+
* @param fn - The function to be executed on the next animation frame
|
|
7
|
+
*/
|
|
8
|
+
export declare const rafSchedule: (fn: () => void) => void;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
let scheduled = false;
|
|
2
|
+
let callback = null;
|
|
3
|
+
/**
|
|
4
|
+
* Schedules a function to be executed on the next animation frame.
|
|
5
|
+
* If a function is already scheduled, the new function will replace it.
|
|
6
|
+
* This helps prevent multiple RAF calls and ensures smooth animations.
|
|
7
|
+
*
|
|
8
|
+
* @param fn - The function to be executed on the next animation frame
|
|
9
|
+
*/
|
|
10
|
+
export const rafSchedule = (fn) => {
|
|
11
|
+
callback = fn;
|
|
12
|
+
if (!scheduled) {
|
|
13
|
+
scheduled = true;
|
|
14
|
+
requestAnimationFrame(() => {
|
|
15
|
+
scheduled = false;
|
|
16
|
+
if (callback) {
|
|
17
|
+
callback();
|
|
18
|
+
callback = null;
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
};
|
|
@@ -60,3 +60,64 @@ export declare const calculateTransformY: (mode: SvelteVirtualListMode, totalIte
|
|
|
60
60
|
* @param {boolean} immediate - Whether to perform the update immediately
|
|
61
61
|
*/
|
|
62
62
|
export declare const updateHeightAndScroll: (state: VirtualListState, setters: VirtualListSetters, immediate?: boolean) => void;
|
|
63
|
+
/**
|
|
64
|
+
* Calculates the average height of visible items in a virtual list.
|
|
65
|
+
*
|
|
66
|
+
* This function optimizes performance by:
|
|
67
|
+
* 1. Using a height cache to store measured item heights
|
|
68
|
+
* 2. Only measuring new items not in the cache
|
|
69
|
+
* 3. Calculating a running average of all measured heights
|
|
70
|
+
*
|
|
71
|
+
* @param {HTMLElement[]} itemElements - Array of currently rendered item elements
|
|
72
|
+
* @param {{ start: number }} visibleRange - Object containing the start index of visible items
|
|
73
|
+
* @param {Record<number, number>} heightCache - Cache of previously measured item heights
|
|
74
|
+
* @param {number} lastMeasuredIndex - Index of the last measured item
|
|
75
|
+
* @param {number} currentItemHeight - Current average item height being used
|
|
76
|
+
*
|
|
77
|
+
* @returns {{
|
|
78
|
+
* newHeight: number,
|
|
79
|
+
* newLastMeasuredIndex: number,
|
|
80
|
+
* updatedHeightCache: Record<number, number>
|
|
81
|
+
* }} Object containing new calculated height, last measured index, and updated cache
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* const result = calculateAverageHeight(
|
|
85
|
+
* itemElements,
|
|
86
|
+
* { start: 0 },
|
|
87
|
+
* {},
|
|
88
|
+
* -1,
|
|
89
|
+
* 40
|
|
90
|
+
* )
|
|
91
|
+
*/
|
|
92
|
+
export declare const calculateAverageHeight: (itemElements: HTMLElement[], visibleRange: {
|
|
93
|
+
start: number;
|
|
94
|
+
}, heightCache: Record<number, number>, lastMeasuredIndex: number, currentItemHeight: number) => {
|
|
95
|
+
newHeight: number;
|
|
96
|
+
newLastMeasuredIndex: number;
|
|
97
|
+
updatedHeightCache: Record<number, number>;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Processes large arrays in chunks to prevent UI blocking.
|
|
101
|
+
*
|
|
102
|
+
* This function implements a progressive processing strategy that:
|
|
103
|
+
* 1. Breaks down large arrays into manageable chunks
|
|
104
|
+
* 2. Processes each chunk asynchronously
|
|
105
|
+
* 3. Reports progress after each chunk
|
|
106
|
+
* 4. Yields to the main thread between chunks
|
|
107
|
+
*
|
|
108
|
+
* @param {any[]} items - Array of items to process
|
|
109
|
+
* @param {number} chunkSize - Number of items to process in each chunk
|
|
110
|
+
* @param {(processed: number) => void} onProgress - Callback for progress updates
|
|
111
|
+
* @param {() => void} onComplete - Callback when all processing is complete
|
|
112
|
+
*
|
|
113
|
+
* @returns {Promise<void>} Resolves when all chunks have been processed
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* await processChunked(
|
|
117
|
+
* largeArray,
|
|
118
|
+
* 50,
|
|
119
|
+
* (processed) => console.log(`Processed ${processed} items`),
|
|
120
|
+
* () => console.log('All items processed')
|
|
121
|
+
* )
|
|
122
|
+
*/
|
|
123
|
+
export declare const processChunked: (items: any[], chunkSize: number, onProgress: (processed: number) => void, onComplete: () => void) => Promise<void>;
|
|
@@ -95,3 +95,101 @@ export const updateHeightAndScroll = (state, setters, immediate = false) => {
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
};
|
|
98
|
+
/**
|
|
99
|
+
* Calculates the average height of visible items in a virtual list.
|
|
100
|
+
*
|
|
101
|
+
* This function optimizes performance by:
|
|
102
|
+
* 1. Using a height cache to store measured item heights
|
|
103
|
+
* 2. Only measuring new items not in the cache
|
|
104
|
+
* 3. Calculating a running average of all measured heights
|
|
105
|
+
*
|
|
106
|
+
* @param {HTMLElement[]} itemElements - Array of currently rendered item elements
|
|
107
|
+
* @param {{ start: number }} visibleRange - Object containing the start index of visible items
|
|
108
|
+
* @param {Record<number, number>} heightCache - Cache of previously measured item heights
|
|
109
|
+
* @param {number} lastMeasuredIndex - Index of the last measured item
|
|
110
|
+
* @param {number} currentItemHeight - Current average item height being used
|
|
111
|
+
*
|
|
112
|
+
* @returns {{
|
|
113
|
+
* newHeight: number,
|
|
114
|
+
* newLastMeasuredIndex: number,
|
|
115
|
+
* updatedHeightCache: Record<number, number>
|
|
116
|
+
* }} Object containing new calculated height, last measured index, and updated cache
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* const result = calculateAverageHeight(
|
|
120
|
+
* itemElements,
|
|
121
|
+
* { start: 0 },
|
|
122
|
+
* {},
|
|
123
|
+
* -1,
|
|
124
|
+
* 40
|
|
125
|
+
* )
|
|
126
|
+
*/
|
|
127
|
+
export const calculateAverageHeight = (itemElements, visibleRange, heightCache, lastMeasuredIndex, currentItemHeight) => {
|
|
128
|
+
const validElements = itemElements.filter((el) => el);
|
|
129
|
+
if (validElements.length === 0) {
|
|
130
|
+
return {
|
|
131
|
+
newHeight: currentItemHeight,
|
|
132
|
+
newLastMeasuredIndex: lastMeasuredIndex,
|
|
133
|
+
updatedHeightCache: heightCache
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const newHeightCache = { ...heightCache };
|
|
137
|
+
// Cache heights for new items
|
|
138
|
+
validElements.forEach((el, i) => {
|
|
139
|
+
const itemIndex = visibleRange.start + i;
|
|
140
|
+
if (!newHeightCache[itemIndex]) {
|
|
141
|
+
newHeightCache[itemIndex] = el.getBoundingClientRect().height;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
// Calculate average from cached heights
|
|
145
|
+
const heights = Object.values(newHeightCache);
|
|
146
|
+
const averageHeight = heights.reduce((sum, h) => sum + h, 0) / heights.length;
|
|
147
|
+
return {
|
|
148
|
+
newHeight: averageHeight > 0 && !isNaN(averageHeight) ? averageHeight : currentItemHeight,
|
|
149
|
+
newLastMeasuredIndex: visibleRange.start,
|
|
150
|
+
updatedHeightCache: newHeightCache
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* Processes large arrays in chunks to prevent UI blocking.
|
|
155
|
+
*
|
|
156
|
+
* This function implements a progressive processing strategy that:
|
|
157
|
+
* 1. Breaks down large arrays into manageable chunks
|
|
158
|
+
* 2. Processes each chunk asynchronously
|
|
159
|
+
* 3. Reports progress after each chunk
|
|
160
|
+
* 4. Yields to the main thread between chunks
|
|
161
|
+
*
|
|
162
|
+
* @param {any[]} items - Array of items to process
|
|
163
|
+
* @param {number} chunkSize - Number of items to process in each chunk
|
|
164
|
+
* @param {(processed: number) => void} onProgress - Callback for progress updates
|
|
165
|
+
* @param {() => void} onComplete - Callback when all processing is complete
|
|
166
|
+
*
|
|
167
|
+
* @returns {Promise<void>} Resolves when all chunks have been processed
|
|
168
|
+
*
|
|
169
|
+
* @example
|
|
170
|
+
* await processChunked(
|
|
171
|
+
* largeArray,
|
|
172
|
+
* 50,
|
|
173
|
+
* (processed) => console.log(`Processed ${processed} items`),
|
|
174
|
+
* () => console.log('All items processed')
|
|
175
|
+
* )
|
|
176
|
+
*/
|
|
177
|
+
export const processChunked = async (items, // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
178
|
+
chunkSize, onProgress, // eslint-disable-line no-unused-vars
|
|
179
|
+
onComplete) => {
|
|
180
|
+
if (!items.length) {
|
|
181
|
+
onComplete();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const processChunk = async (startIdx) => {
|
|
185
|
+
const endIdx = Math.min(startIdx + chunkSize, items.length);
|
|
186
|
+
onProgress(endIdx);
|
|
187
|
+
if (endIdx < items.length) {
|
|
188
|
+
setTimeout(() => processChunk(endIdx), 0);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
onComplete();
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
await processChunk(0);
|
|
195
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanspeak/svelte-virtual-list",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "A lightweight, high-performance virtual list component for Svelte 5 that renders large datasets with minimal memory usage. Features include dynamic height support, smooth scrolling, TypeScript support, and efficient DOM recycling. Ideal for infinite scrolling lists, data tables, chat interfaces, and any application requiring the rendering of thousands of items without compromising performance. Zero dependencies and fully customizable.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"svelte": "./dist/index.js",
|