Retour au classement

inokawa/virtua

TypeScriptinokawa.github.io/virtua/

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

infinite-scrollperformancereactreact-componentvirtualizationvirtualizedwindowingvirtual-scrollscrollingvueheadlessuisolid
Croissance des étoiles
Étoiles
3.6k
Forks
110
Croissance hebdomadaire
Issues
58
1k2k3k
mars 2023avr. 2024juin 2025juil. 2026
Artefactsnpmnpm install virtua
README

virtua

npm npm bundle size npm Best of JS Ask DeepWiki check demo

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

example

If you want to check the difference with the alternatives right away, see comparison section.

Motivation

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, reduce synchronous layout recalculation, reduce visual jumps on repaint, optimize with CSS, optimize for JIT, 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 tree-shakeable.
  • 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: React, Vue, Solid and Svelte are supported. We could support other frameworks in the future.

Demo

https://inokawa.github.io/virtua/

Install

npm install virtua

If you use this lib in legacy browsers which does not have ResizeObserver, you should use polyfill.

Getting started

React

react >= 16.14 is required.

If you use ESM and webpack 5, use react >= 18 to avoid Can't resolve react/jsx-runtime error.

Vertical scroll

import { VList } from "virtua";

// children
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>
  );
};

// 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) => (
        <div
          key={i}
          style={{
            height: d,
            borderBottom: "solid 1px gray",
            background: "white",
          }}
        >
          {i}
        </div>
      )}
    </VList>
  );
};

Horizontal scroll

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>
  );
};

Customization

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 (
    <div style={{ overflowY: "auto", overflowAnchor: "none", height: 800 }}>
      <div style={{ height: 40 }}>header</div>
      <Virtualizer startMargin={40}>
        {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>
        ))}
      </Virtualizer>
    </div>
  );
};

Window scroll

import { WindowVirtualizer } from "virtua";

export const App = () => {
  return (
    <div style={{ padding: 200 }}>
      <WindowVirtualizer>
        {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>
        ))}
      </WindowVirtualizer>
    </div>
  );
};

Vertical and horizontal scroll

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,
            background: "white",
            borderLeft: colIndex !== 0 ? "solid 1px gray" : undefined,
            borderTop: rowIndex !== 0 ? "solid 1px gray" : undefined,
          }}
        >
          {rowIndex} / {colIndex}
        </div>
      )}
    </VGrid>
  );
};

Vue

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 }">
    <div
      :key="index"
      :style="{
        height: item + 'px',
        background: 'white',
        borderBottom: 'solid 1px #ccc',
      }"
    >
      {{ index }}
    </div>
  </VList>
</template>

Solid

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) => (
        <div
          style={{
            height: d + "px",
            "border-bottom": "solid 1px #ccc",
            background: "#fff",
          }}
        >
          {i()}
        </div>
      )}
    </VList>
  );
};

Svelte

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)}
    <div
      style="
        height: {item}px;
        background: white;
        border-bottom: solid 1px #ccc;
      "
    >
      {index}
    </div>
  {/snippet}
</VList>

Other bindings

Documentation

FAQs

Is there any way to improve performance further?

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 (
  <div>
    <div>position: {position}</div>
    <VList onScroll={(offset) => setPosition(offset)}>{elements}</VList>
  </div>
);

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.

What is 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.

Why my items are squashed(or rendered inconsistently) on resize/add/remove?

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.

Why VListHandle.viewportSize is 0 on mount?

viewportSize will be calculated by ResizeObserver so it's 0 until the first measurement.

What is 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.

Comparison

Features

virtua react-virtuoso react-window react-virtualized @tanstack/react-virtual
Bundle size npm bundle size npm bundle size npm bundle size npm bundle size npm bundle size
Vertical scroll 🟠 (needs customization)
Horizontal scroll 🟠 (needs customization)
Horizontal scroll in RTL direction 🟠 (isRtl)
Grid (Virtualization for two dimension) 🟠 (experimental_VGrid) ✅ (Grid) ✅ (Grid) 🟠 (needs customization)
Table 🟠 (needs customization) ✅ (TableVirtuoso) 🟠 (Supported) 🟠 (Table but it's built with div) 🟠 (needs customization)
Masonry ✅ (VirtuosoMasonry) ✅ (Masonry) 🟠 (needs customization)
Window scroller ✅ (WindowVirtualizer) ✅ (WindowScroller) ✅ (useWindowVirtualizer)
Dynamic list size 🟠 (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)
Reverse scroll
Reverse scroll in iOS Safari 🟠 (user must release scroll) 🟠 (has glitch with unknown sized items)
Infinite scroll 🟠 (needs react-window-infinite-loader) 🟠 (needs InfiniteLoader)
Reverse (bi-directional) infinite scroll
Scroll restoration ✅ (getState)
Smooth scroll
SSR support
Render React Server Components (RSC) as children 🟠(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

Benchmark

WIP

Contribute

All contributions are welcome. If you find a problem, feel free to create an issue or a PR. If you have a question, ask in discussions.

Making a Pull Request

  1. Fork this repo.
  2. Run npm install.
  3. Commit your fix.
  4. Make a PR and confirm all the CI checks passed.
Dépôts similaires
meliorence/react-native-snap-carousel

Swiper/carousel component for React Native featuring previews, multiple layouts, parallax images, performant handling of huge numbers of items, and more. Compatible with Android & iOS.

JavaScriptnpmBSD 3-Clause "New" or "Revised" Licensecarouselswiper
10.5k2.3k
WenchaoD/FSPagerView

FSPagerView is an elegant Screen Slide Library. It is extremely helpful for making Banner View、Product Show、Welcome/Guide Pages、Screen/ViewController Sliders.

SwiftMIT Licenseswiftobjective-c
7.4k1.1k
Devlight/InfiniteCycleViewPager

Infinite cycle ViewPager with two-way orientation and interactive effect.

JavaMavenApache License 2.0infinite-scrollviewpager
5.7k821
tangbc/vue-virtual-scroll-list

⚡️A vue component support big amount data list with high render performance and efficient.

JavaScriptnpmMIT Licensevirtual-listbig-data
tangbc.github.io/vue-virtual-scroll-list
4.5k596
hoothin/UserScripts

Greasemonkey scripts ( Pagetual / Picviewer CE+ / DownloadAllContent ) 油猴腳本集 ユーザースクリプト集

JavaScriptnpmauto-scrollautopager
greasyfork.org/users/8227-hoothin
4.2k589
clauderic/react-infinite-calendar

✨ Infinite scrolling date-picker built with React, with localization, range selection, themes, keyboard support, and more.

JavaScriptnpmMIT Licensereactcalendar
clauderic.github.io/react-infinite-calendar/
4k401
setchi/FancyScrollView

A versatile Unity scroll view component that enables highly flexible animations.

C#MIT Licenseunityunity3d
deepwiki.com/setchi/FancyScrollView
3.5k431
dohooo/react-native-reanimated-carousel

🎠 React Native swiper/carousel component, fully implemented using reanimated v2, support to iOS/Android/Web. (Swiper/Carousel)

TypeScriptnpmMIT Licensereact-nativecarousel
react-native-reanimated-carousel.vercel.app
3.4k351
ankeetmaini/react-infinite-scroll-component

An awesome Infinite Scroll component in react.

TypeScriptnpmMIT Licensereactinfinite-scroll
react-infinite-scroll-component.netlify.com
3.1k333
fairygui/FairyGUI-unity

A flexible UI framework for Unity

C#MIT Licenseunity3dunity3d-plugin
fairygui.com
3k669
mypaint/mypaint

MyPaint is a simple drawing and painting program that works well with Wacom-style graphics tablets.

PythonPyPIGNU General Public License v2.0paintingdrawing
mypaint.app
3k448
eggswift/pull-to-refresh

#Busy Re-Building....# An easy way to use pull to refresh and infinite scrolling in Swift. Pod 'ESPullToRefresh'

SwiftMIT Licenserefresherpull-to-refresh
1.8k255