A zero-config, fast and small (~3kB) virtual list (and grid) component for React, Vue, Solid and Svelte.

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...
https://inokawa.github.io/virtua/
npm install virtua
If you use this lib in legacy browsers which does not have ResizeObserver, you should use polyfill.
react >= 16.14 is required.
If you use ESM and webpack 5, use react >= 18 to avoid Can't resolve react/jsx-runtime error.
import { VList } from "virtua";
// children
export const App = () => {
return (
<VList style={{ height: 800 }}>
{Array.from({ length: 1000 }).map((_, i) => (
{i}
))}
</VList>
);
};
// or render prop
export const App = () => {
const items = Array.from({ length: 1000 }).map(
() => Math.floor(Math.random() * 10) * 10 + 10,
);
return (
<VList data={items} style={{ height: 800 }}>
{(d, i) => (
{i}
)}
</VList>
);
};
import { VList } from "virtua";
export const App = () => {
return (
<VList style={{ height: 400 }} horizontal>
{Array.from({ length: 1000 }).map((_, i) => (
{i}
))}
</VList>
);
};
VList is a recommended solution which works like a drop-in replacement of simple list built with scrollable div (or removed virtual-scroller element). For more complicated styling or markup, use Virtualizer.
import { Virtualizer } from "virtua";
export const App = () => {
return (
header
<Virtualizer startMargin={40}>
{Array.from({ length: 1000 }).map((_, i) => (
{i}
))}
</Virtualizer>
);
};
import { WindowVirtualizer } from "virtua";
export const App = () => {
return (
<WindowVirtualizer>
{Array.from({ length: 1000 }).map((_, i) => (
{i}
))}
</WindowVirtualizer>
);
};
import { experimental_VGrid as VGrid } from "virtua";
export const App = () => {
return (
<VGrid style={{ height: 800 }} row={1000} col={500}>
{({ rowIndex, colIndex }) => (
{rowIndex} / {colIndex}
)}
</VGrid>
);
};
vue >= 3.2 is required.
<script setup>
import { VList } from "virtua/vue";
const sizes = [20, 40, 180, 77];
const data = Array.from({ length: 1000 }).map((_, i) => sizes[i % 4]);
</script>
<template>
<VList :data="data" :style="{ height: '800px' }" #default="{ item, index }">
{{ index }}
</VList>
</template>
solid-js >= 1.0 is required.
import { VList } from "virtua/solid";
export const App = () => {
const sizes = [20, 40, 180, 77];
const data = Array.from({ length: 1000 }).map((_, i) => sizes[i % 4]);
return (
<VList data={data} style={{ height: "800px" }}>
{(d, i) => (
{i()}
)}
</VList>
);
};
svelte >= 5.0 is required.
<script lang="ts">
import { VList } from "virtua/svelte";
const sizes = [20, 40, 180, 77];
const data = Array.from({ length: 1000 }).map((_, i) => sizes[i % 4] );
</script>
<VList {data} style="height: 100vh;" getKey={(_, i) => i}>
{#snippet children(item, index)}
{index}
{/snippet}
</VList>
In complex usage, especially if you re-render frequently the parent of virtual scroller or the children are tons of items, children element creation can be a performance bottle neck. That's because creating React elements is fast enough but not free and new React element instances break some of memoization inside virtual scroller.
One solution is memoization with useMemo. You can use it to reduce computation and keep the elements' instance the same. And if you want to pass state from parent to the items, using context instead of props may be better because it doesn't break the memoization.
const elements = useMemo(
() => tooLongArray.map((d) => <Component key={d.id} {...d} />),
[tooLongArray],
);
const [position, setPosition] = useState(0);
return (
position: {position}
<VList onScroll={(offset) => setPosition(offset)}>{elements}</VList>
);
The other solution is using render prop as children to create elements lazily. It will effectively reduce cost on start up when you render many items (>1000). An important point is that newly created elements from render prop will disable optimization possible with cached element instances. We recommend using memoized function or component to reduce calculation and re-rendering during scrolling.
// memoize render function with some memoization library
import memoize from "memoize";
const renderItem = memoize((item: Data) => {
return <Component key={item.id} data={item} />;
});
<VList data={items}>{renderItem}</VList>;
// memoize component with React.memo
import { memo } from "react";
const Component = memo(HeavyItem);
<VList data={items}>
{(item) => {
return <Component key={item.id} data={item} />;
}}
</VList>;
Decreasing bufferSize prop may also improve perf in case that components are large and heavy.
Virtua try to suppress glitch caused by resize as much as possible, but it will also require additional work. If your item contains something resized often, such as lazy loaded image, we recommend to set height or min-height to it if possible.
ResizeObserver loop completed with undelivered notifications. error?It may be dispatched by ResizeObserver in this lib as described in spec, and this is a common problem with ResizeObserver. If it bothers you, you can safely ignore it.
Especially for webpack-dev-server, you can filter out the specific error with devServer.client.overlay.runtimeErrors option.
Maybe you forgot to pass key prop to each items, or the keys are not unique. Item sizes are stored per key.
And do not use index of items as key, especially when you want to toggle shift prop to true. Prepending will increment every indexes of items and that will cause unexpected behavior.
VListHandle.viewportSize is 0 on mount?viewportSize will be calculated by ResizeObserver so it's 0 until the first measurement.
Cannot find module 'virtua/vue(solid|svelte)' or its corresponding type declarations error?This package uses exports of package.json for entry point of Vue/Solid/Svelte adapter. This field can't be resolved in TypeScript with moduleResolution: node. Try moduleResolution: bundler or moduleResolution: nodenext instead.
| virtua | react-virtuoso | react-window | react-virtualized | @tanstack/react-virtual | |
|---|---|---|---|---|---|
| Bundle size |
$ claude mcp add virtua \
-- python -m otcore.mcp_server <graph>