A zero-config, fast and small (~3kB) virtual list (and grid) component for React.
If you want to check the difference with the alternatives right away, see comparison section.
This project is a challenge to rethink virtualization. The goals are...
- Zero-config virtualization: This library is designed to give the best performance without configuration. It also handles common hard things in the real world (dynamic size measurement, scroll position adjustment while reverse scrolling and imperative scrolling, iOS support, etc).
- Fast: Natural virtual scrolling needs optimization in many aspects (eliminate frame drops by reducing CPU usage and GC and layout recalculation, reduce visual jumps on repaint, optimize with CSS, optimize for frameworks, etc). We are trying to combine the best of them.
- Small: Its bundle size should be small as much as possible to be friendly with modern web development. Currently each components are ~3kB gzipped and the total is ~5kB gzipped.
- Flexible: Aiming to support many usecases - fixed size, dynamic size, horizontal scrolling, reverse scrolling, RTL, mobile, infinite scrolling, scroll restoration, DnD, keyboard navigation, sticky, placeholder and more. See live demo.
- Framework agnostic (WIP): Currently only for React but we could support Vue, Svelte, Solid, Angular, Web Components and more in the future.
https://inokawa.github.io/virtua/
npm install virtua
- react >= 16.14
If you use ESM and webpack 5, use react >= 18 to avoid Can't resolve react/jsx-runtime
error.
If you use this lib in legacy browsers which does not have ResizeObserver, you should use polyfill.
import { VList } from "virtua";
export const App = () => {
return (
<VList style={{ height: 800 }}>
{Array.from({ length: 1000 }).map((_, i) => (
<div
key={i}
style={{
height: Math.floor(Math.random() * 10) * 10 + 10,
borderBottom: "solid 1px gray",
background: "white",
}}
>
{i}
</div>
))}
</VList>
);
};
import { VList } from "virtua";
export const App = () => {
return (
<VList style={{ height: 400 }} horizontal>
{Array.from({ length: 1000 }).map((_, i) => (
<div
key={i}
style={{
width: Math.floor(Math.random() * 10) * 10 + 10,
borderRight: "solid 1px gray",
background: "white",
}}
>
{i}
</div>
))}
</VList>
);
};
import { WVList } from "virtua";
export const App = () => {
return (
<div style={{ padding: 200 }}>
<WVList>
{Array.from({ length: 1000 }).map((_, i) => (
<div
key={i}
style={{
height: Math.floor(Math.random() * 10) * 10 + 10,
borderBottom: "solid 1px gray",
background: "white",
}}
>
{i}
</div>
))}
</WVList>
</div>
);
};
import { experimental_VGrid as VGrid } from "virtua";
export const App = () => {
return (
<VGrid style={{ height: 800 }} row={1000} col={500}>
{({ rowIndex, colIndex }) => (
<div
style={{
width: ((colIndex % 3) + 1) * 100,
border: "solid 1px gray",
background: "white",
}}
>
{rowIndex} / {colIndex}
</div>
)}
</VGrid>
);
};
This library is marked as a Client Component. You can render RSC as children of VList or WVList.
// page.tsx in App Router of Next.js
export default async () => {
const articles = await fetchArticles();
return (
<div>
<div>This is Server Component</div>
<VList style={{ height: 300 }}>
{articles.map((a) => (
<div key={a.id} style={{ border: "solid 1px gray", height: 80 }}>
{a.content}
</div>
))}
</VList>
</div>
);
};
And see examples for more usages.
As a trade-off to be compatible with React's built-in elements like div
, virtua doesn't have optimization possible by using render prop which some of other virtualization libraries for React have. That also means common optimization techniques for non-virtualized list (memo
, useMemo
, context
, etc) work for this lib!
In complex usage, children element generation can be a performance bottle neck if you re-render frequently the parent of virtual scroller and the children are tons of items. That's because React element generation is fast enough but not free and new React element instance breaks some of memoization inside virtual scroller. In that case use useMemo
to reduce computation and keep the elements' instance the same.
const elements = useMemo(
() => tooLongArray.map((d) => <Component key={d.id} {...d} />),
[tooLongArray]
);
const [position, setPosition] = useState(0);
return (
<div>
<div>position: {position}</div>
<VList onScroll={(offset) => setPosition(offset)}>{elements}</VList>
</div>
);
And if you want to pass the state to the items, using context
instead of props may be better because it doesn't break the useMemo.
It may be dispatched by ResizeObserver in this lib as described in spec. If it bothers you, you can safely ignore it.
virtua | react-virtuoso | react-window | react-virtualized | @tanstack/react-virtual | react-tiny-virtual-list | react-cool-virtual | |
---|---|---|---|---|---|---|---|
Bundle size | 5.0kB gzipped | 16.9kB gzipped | 6.4kB gzipped | 27.3kB gzipped | 2.3kB gzipped | 3.7kB gzipped | 3.1kB gzipped |
Vertical scroll | β | β | β | β | π (needs customization) | β | π (needs customization) |
Horizontal scroll | β | β | β (may be dropped in v2) | β | π (needs customization) | β | π (needs customization) |
Grid (Virtualization for two dimension) | π (experimental_VGrid) | β | β (FixedSizeGrid / VariableSizeGrid) | β (Grid) | π (needs customization) | β | π (needs customization) |
Table | π (needs customization) | β (TableVirtuoso) | π (needs customization) | β (Table) | π (needs customization) | β | π (needs customization) |
Window scroller | β (WVList) | β | β | β (WindowScroller) | β | β | β |
Dynamic list size | β | β | π (needs AutoSizer) | π (needs AutoSizer) | β | β | β |
Dynamic item size | β | β | π (needs additional codes and has wrong destination when scrolling to item imperatively) | π (needs CellMeasurer and has wrong destination when scrolling to item imperatively) | π (has wrong destination when scrolling to item imperatively) | β | π (has wrong destination when scrolling to item imperatively) |
Reverse scroll | β | β | β | β | β | β | β |
Reverse scroll in iOS Safari | β | π (has glitch with unknown sized items) | β | β | β | β | β |
Infinite scroll | β | β | π (needs react-window-infinite-loader) | π (needs InfiniteLoader) | β | β | β |
Reverse (bi-directional) infinite scroll | β | β | β | β | β | β | π (has startItem method but its scroll position can be inaccurate) |
Scroll restoration | β | β (getState) | β | β | β | β | β |
Smooth scroll | β | β | β | β | β | β | β |
RTL support | β | β | β (may be dropped in v2) | β | β | β | β |
SSR support | β | β | β | β | β | β | β |
Render React Server Components (RSC) as children | β | β | β | β | π (needs customization) | β | π (needs customization) |
Display exceeding browser's max element size limit | β | β | β | β | β | β | β |
- β - Built-in supported
- π - Supported but partial, limited or requires some user custom code
- β - Not officially supported
WIP
Basically by design, this library uses children instead of render prop like other virtual scrollers. It reduces rerender during scrolling with caching element instances. However it may make component's mount slower if you display million items.
All contributions are welcome. If you find a problem, feel free to create an issue or a PR.
- Fork this repo.
- Run
npm install
. - Commit your fix.
- Make a PR and confirm all the CI checks passed.