clusterize-lazy
v1.1.0
Published
Lightweight virtual list helper for the browser.
Downloads
19
Maintainers
Readme
Clusterize-Lazy
Vanilla-JS virtual list with lazy loading and initial skeletons.
Clusterize-Lazy lets you render millions of rows in a scrollable
container while downloading data only for what the user can actually
see. Its goal is an easy, framework-agnostic API that just works in
any modern browser - no build step required.
Small footprint, great DX - powered by @tanstack/virtual-core
Features
- Single dependency - relies on the rock-solid engine
@tanstack/virtual-core(thanks Tanner & the TanStack team!) - Dynamic row height - actual DOM sizes are measured automatically
- Lazy loading + skeletons - smooth UX even on shaky connections
- Typed from the ground up - shipped
.d.tsworks in ESM browsers and legacy browsers (with polyfills) - Batteries included - debug logging, auto cache eviction, progress callback
Live demo
Test it instantly (no transpiler):
https://joobypm.github.io/clusterize-lazy/examples/quotes.html
Source lives in
docs/examples/
Installation
pnpm / npm / Yarn
pnpm add clusterize-lazyimport Clusterize from 'clusterize-lazy';
const cluster = Clusterize({...});<script> tag (UMD)
<script src="https://unpkg.com/clusterize-lazy/dist/index.iife.js"></script>
<script>
const cluster = Clusterize.default({...});
</script>30-second example
<div id="scroll" style="height: 320px; overflow: auto">
<div id="content"></div>
</div>
<script type="module">
import Clusterize from '/dist/index.esm.js';
function fetchRows(offset, size = 40) {
return fetch(`/api/items?skip=${offset}&limit=${size}`).then((r) => r.json());
}
const cluster = Clusterize({
rowHeight: 32,
scrollElem: document.getElementById('scroll'),
contentElem: document.getElementById('content'),
fetchOnInit: async () => {
const rows = await fetchRows(0);
return { totalRows: 50_000, rows };
},
fetchOnScroll: fetchRows,
renderSkeletonRow: (h, i) => `<div class="skeleton" style="height:${h}px"></div>`,
renderRaw: (i, row) => `<div>${i + 1}. ${row.title}</div>`,
});
</script>Mutation example
// Enable mutations with buildIndex
const cluster = Clusterize({
rowHeight: 40,
buildIndex: true, // Enable ID-based operations
primaryKey: 'id', // Use 'id' field as primary key
scrollElem: document.getElementById('scroll'),
contentElem: document.getElementById('content'),
fetchOnInit: () => Promise.resolve([
{ id: 1, name: 'Alice', status: 'active' },
{ id: 2, name: 'Bob', status: 'inactive' },
{ id: 3, name: 'Charlie', status: 'active' }
]),
fetchOnScroll: () => Promise.resolve([]),
renderSkeletonRow: (h) => `<div style="height:${h}px">Loading...</div>`,
renderRaw: (i, row) => `<div>${row.name} (${row.status})</div>`
});
// Add new rows
cluster.insert([{ id: 4, name: 'David', status: 'active' }], 1);
// Update existing row by ID
cluster.update([{ id: 2, data: { id: 2, name: 'Bob', status: 'active' } }]);
// Remove rows by ID
cluster.remove([1, 3]);Quick reference
| Option / method | Type / default | Purpose |
| --------------------------------- | -------------------------------------------- | ---------------------------------------------- |
| required | | |
| rowHeight | number | Fixed row estimate (px) |
| fetchOnInit() | () ⇒ Promise<Row[] \| { totalRows, rows }> | First data batch or rows + total count |
| fetchOnScroll(offset) | (number) ⇒ Promise<Row[]> | Fetches when a gap becomes visible |
| renderSkeletonRow(height,index) | (number,number) ⇒ string | Placeholder HTML |
| optional | | |
| renderRaw(index,data) | (number,Row) ⇒ string · undefined | Row renderer for object data |
| buffer | 5 | Rows rendered above/under viewport |
| prefetchRows | buffer | Rows fetched ahead of viewport |
| debounceMs | 120 | Debounce between scroll & fetch |
| cacheTTL | 300 000 | Milliseconds before a cached row is stale |
| autoEvict | false | Drop stale rows automatically |
| showInitSkeletons | true | Paint skeletons immediately before first fetch |
| debug | false | Console debug output |
| scrollingProgress(cb) | (firstVisible:number) ⇒ void | Fires on every render |
| buildIndex | false | Build ID-to-index map for mutations |
| primaryKey | 'id' | Property name for primary key |
| methods | | |
| refresh() | void | Force re-render |
| scrollToRow(idx, smooth?) | void ( true = smooth) | Programmatic scroll |
| getLoadedCount() | number | How many rows are cached |
| destroy() | void | Tear down listeners & cache |
| insert(rows, at?) | void | Insert rows at position (default: 0) |
| update(patches) | void | Update rows by ID or index |
| remove(keys) | void | Remove rows by ID or index |
| _dump() | { cache, index } | Debug helper for internal state |
See docs/API.md for the full contract.
Contributing
git clone https://github.com/JoobyPM/clusterize-lazy.git
pnpm i
pnpm test # vitest + jsdom
pnpm build # tsup + esbuildFormatting & linting are handled by Deno (deno fmt, deno lint).
Please follow Conventional Commits; releases are automated.
Acknowledgements
Huge shout-out to [@tanstack/virtual-core] - Clusterize-Lazy is basically a thin, opinionated shell around this fantastic engine. If you need a React / Vue / Solid binding or more power, use the TanStack package directly and consider sponsoring the project.
License
MIT © 2025 JoobyPM
