clusterize-lazy
TypeScript icon, indicating that this package has built-in type declarations

1.1.0 • Public • Published

Clusterize-Lazy

Vanilla-JS virtual list with lazy loading and initial skeletons.

gzip MIT

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

demo

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.ts works 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-lazy
import 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 + esbuild

Formatting & 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

Package Sidebar

Install

npm i clusterize-lazy

Weekly Downloads

17

Version

1.1.0

License

MIT

Unpacked Size

196 kB

Total Files

9

Last publish

Collaborators

  • jooby_pm