Skip to content
forked from inokawa/virtua

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

License

Notifications You must be signed in to change notification settings

kayasukru/virtua

Β 
Β 

Repository files navigation

virtua

npm npm bundle size npm check demo

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

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, etc).
  • Fast: Scrolling without frame drop needs optimization in many aspects (reduce CPU usage, reduce GC, reduce layout recalculation, 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 direction, sticky, infinite scrolling, placeholder, scrollTo, scroll restoration, dnd, table, 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.

Demo

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

Install

npm install virtua

Requirements

  • 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.

Usage

Vertical scroll

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

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

Vertical and horizontal scroll

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

Window scroll

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

React Server Components (RSC) support

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.

Documentation

FAQs

Is there any way to improve performance further?

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, because React element generation is 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(
  () => array.map((d) => <Component key={d.id} {...d} />),
  [array]
);
const [position, setPosition] = useState(0);
return (
  <div>
    <div>position: {position}</div>
    <VList onScroll={(offset) => setPosition(offset)}>{elements}</VList>
  </div>
);

What is ResizeObserver loop completed with undelivered notifications. error?

It may be dispatched by ResizeObserver in this lib as described in spec. If it bothers you, you can safely ignore it.

Comparison

Features

virtua react-virtuoso react-window react-virtualized @tanstack/react-virtual react-cool-virtual
Bundle size 4.7kB gzipped 16.3kB gzipped 6.4kB gzipped 27.3kB gzipped 2.3kB gzipped 3.1kB gzipped
Vertical scroll βœ… βœ… βœ… βœ… 🟠 (needs customization) 🟠 (needs customization)
Horizontal scroll βœ… ❌ βœ… βœ… 🟠 (needs customization) 🟠 (needs customization)
Grid (Virtualization for two dimension) βœ… (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 ❌ βœ… ❌ ❌ ❌ ❌
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 direction βœ… ❌ βœ… ❌ ❌ ❌
SSR support βœ… βœ… βœ… βœ… βœ… βœ…
React Server Components (RSC) support βœ… ? ? ? βœ… (hook) βœ… (hook)
Display exceeding browser's max element size limit ❌ ❌ ❌ βœ… ❌ ❌
  • βœ… - Built-in supported
  • 🟠 - Supported but partial, limited or requires some user custom code
  • ❌ - Not officially supported

Benchmarks

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.

Contribute

All contributions are welcome. If you find a problem, feel free to create an issue or a PR.

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.

About

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

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • TypeScript 98.7%
  • Other 1.3%