https://github.com/joobypm/clusterize-lazy
⚡️ Virtual-scroll helper for browsers. Renders millions of rows smoothly and lazily fetches only what’s visible. Tiny bundle, zero deps beyond @tanstack/virtual-core, fully typed with TypeScript, works in every modern browser.
https://github.com/joobypm/clusterize-lazy
frontend infinite-scroll javascript lazy-loading scroll-performance tanstack-virtual typescript vanilla-js virtual-list virtual-scroll
Last synced: 7 days ago
JSON representation
⚡️ Virtual-scroll helper for browsers. Renders millions of rows smoothly and lazily fetches only what’s visible. Tiny bundle, zero deps beyond @tanstack/virtual-core, fully typed with TypeScript, works in every modern browser.
- Host: GitHub
- URL: https://github.com/joobypm/clusterize-lazy
- Owner: JoobyPM
- License: mit
- Created: 2025-06-12T23:35:38.000Z (9 months ago)
- Default Branch: main
- Last Pushed: 2025-06-22T09:01:44.000Z (9 months ago)
- Last Synced: 2025-09-22T01:51:43.717Z (6 months ago)
- Topics: frontend, infinite-scroll, javascript, lazy-loading, scroll-performance, tanstack-virtual, typescript, vanilla-js, virtual-list, virtual-scroll
- Language: TypeScript
- Homepage: https://joobypm.github.io/clusterize-lazy/examples/quotes.html
- Size: 1.89 MB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
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](https://github.com/TanStack/virtual)
## 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):\
> Source lives in `docs/examples/`
## Installation
### pnpm / npm / Yarn
```bash
pnpm add clusterize-lazy
```
```js
import Clusterize from 'clusterize-lazy';
const cluster = Clusterize({...});
```
### `` tag (UMD)
```html
<script src="https://unpkg.com/clusterize-lazy/dist/index.iife.js">
const cluster = Clusterize.default({...});
```
## 30-second example
```html
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>`,
});
```
## Mutation example
```js
// 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) => `
Loading...`,
renderRaw: (i, row) => `${row.name} (${row.status})`
});
// 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` | First data batch _or_ rows + total count |
| `fetchOnScroll(offset)` | `(number) ⇒ Promise` | 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](docs/API.md) for the full contract.
## Contributing
```bash
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