Skip to content

Latest commit

Β 

History

1,977 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

virtua

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

A zero-config, fast and small virtual list, grid and masonry component for React, Vue, Solid, Svelte and Angular.

list example grid 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 components start from ~3kB gzipped and are tree-shakeable.
  • Flexible: Aiming to support many usecases - fixed size, dynamic size, horizontal scrolling, reverse scrolling, table, masonry, RTL, mobile, infinite scrolling, scroll restoration, DnD, keyboard navigation, sticky and more. See live demo.
  • Framework agnostic: React, Vue, Solid, Svelte and Angular are supported. We could support other frameworks in the future.

Demo

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

Install

npm install virtua

If you want to support legacy browsers, you need polyfills.

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.

import { VList } from "virtua";

const sizes = [20, 40, 80, 77];
const data = Array.from({ length: 1000 }).map((_, i) => ({
  id: i,
  size: sizes[i % 4],
}));

export const App = () => {
  return (
    <VList data={data} style={{ height: 800 }}>
      {(item) => (
        <div
          key={item.id}
          style={{
            height: item.size,
            background: "white",
            borderBottom: "solid 1px #ccc",
          }}
        >
          {item.id}
        </div>
      )}
    </VList>
  );
};

You can also pass elements as children directly instead of data.

Vue

vue >= 3.2 is required.

<script setup>
import { VList } from "virtua/vue";

const sizes = [20, 40, 80, 77];
const data = Array.from({ length: 1000 }).map((_, i) => ({
  id: i,
  size: sizes[i % 4],
}));
</script>

<template>
  <VList :data="data" :style="{ height: '800px' }" #default="{ item }">
    <div
      :key="item.id"
      :style="{
        height: item.size + 'px',
        background: 'white',
        borderBottom: 'solid 1px #ccc',
      }"
    >
      {{ item.id }}
    </div>
  </VList>
</template>

Solid

solid-js >= 1.0 is required.

import { VList } from "virtua/solid";

const sizes = [20, 40, 80, 77];
const data = Array.from({ length: 1000 }).map((_, i) => ({
  id: i,
  size: sizes[i % 4],
}));

export const App = () => {
  return (
    <VList data={data} style={{ height: "800px" }}>
      {(item) => (
        <div
          style={{
            height: item.size + "px",
            background: "white",
            "border-bottom": "solid 1px #ccc",
          }}
        >
          {item.id}
        </div>
      )}
    </VList>
  );
};

Svelte

svelte >= 5.0 is required.

<script lang="ts">
  import { VList } from "virtua/svelte";

  const sizes = [20, 40, 80, 77];
  const data = Array.from({ length: 1000 }).map((_, i) => ({
    id: i,
    size: sizes[i % 4],
  }));
</script>

<VList {data} style="height: 800px;" getKey={(item) => item.id}>
  {#snippet children(item)}
    <div
      style="
        height: {item.size}px;
        background: white;
        border-bottom: solid 1px #ccc;
      "
    >
      {item.id}
    </div>
  {/snippet}
</VList>

Angular

@angular/core >= 20 is required. The components are zoneless-ready and work with both zone.js and provideZonelessChangeDetection.

import { Component } from "@angular/core";
import { VList } from "virtua/angular";

const sizes = [20, 40, 80, 77];

@Component({
  selector: "app-root",
  imports: [VList],
  template: `
    <virtua-vlist [data]="data" [getKey]="getKey" style="height: 800px;">
      <ng-template let-item>
        <div
          [style.height.px]="item.size"
          style="background: white; border-bottom: solid 1px #ccc;"
        >
          {{ item.id }}
        </div>
      </ng-template>
    </virtua-vlist>
  `,
})
export class App {
  protected readonly data = Array.from({ length: 1000 }).map((_, i) => ({
    id: i,
    size: sizes[i % 4],
  }));
  protected readonly getKey = (item: { id: number }) => item.id;
}

Other bindings

Usage

The examples are written in React, but the same components and props are available in all frameworks.

Horizontal scroll

import { VList } from "virtua";

const sizes = [20, 40, 80, 77];
const data = Array.from({ length: 1000 }).map((_, i) => ({
  id: i,
  size: sizes[i % 4],
}));

export const App = () => {
  return (
    <VList data={data} style={{ height: 400 }} horizontal>
      {(item) => (
        <div
          key={item.id}
          style={{
            width: item.size,
            background: "white",
            borderRight: "solid 1px #ccc",
          }}
        >
          {item.id}
        </div>
      )}
    </VList>
  );
};

Custom scroll container

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";

const sizes = [20, 40, 80, 77];
const data = Array.from({ length: 1000 }).map((_, i) => ({
  id: i,
  size: sizes[i % 4],
}));
const headerHeight = 40;

export const App = () => {
  return (
    <div
      style={{
        height: 800,
        overflowY: "auto",
        // opt out browser's scroll anchoring on header/footer because it will conflict with scroll anchoring of virtualizer
        overflowAnchor: "none",
      }}
    >
      <div style={{ height: headerHeight }}>header</div>
      <Virtualizer data={data} startMargin={headerHeight}>
        {(item) => (
          <div
            key={item.id}
            style={{
              height: item.size,
              background: "white",
              borderBottom: "solid 1px #ccc",
            }}
          >
            {item.id}
          </div>
        )}
      </Virtualizer>
      <div style={{ height: 600 }}>footer</div>
    </div>
  );
};

Window scroll

import { WindowVirtualizer } from "virtua";

const sizes = [20, 40, 80, 77];
const data = Array.from({ length: 1000 }).map((_, i) => ({
  id: i,
  size: sizes[i % 4],
}));

export const App = () => {
  return (
    <div style={{ padding: 200 }}>
      <WindowVirtualizer data={data}>
        {(item) => (
          <div
            key={item.id}
            style={{
              height: item.size,
              background: "white",
              borderBottom: "solid 1px #ccc",
            }}
          >
            {item.id}
          </div>
        )}
      </WindowVirtualizer>
    </div>
  );
};

Tabular data

For data with rows and columns such as a table, use VGrid. It virtualizes on both axes.

import { VGrid } from "virtua";

const columns = [
  { key: "id", width: 80 },
  { key: "name", width: 200 },
  { key: "email", width: 300 },
  { key: "age", width: 80 },
  { key: "joined", width: 160 },
  { key: "bio", width: 480 },
] as const;

const rows = [
  // the header row has no data
  null,
  ...Array.from({ length: 10000 }).map((_, i) => ({
    id: i,
    name: `User ${i}`,
    email: `user${i}@example.com`,
    age: 20 + (i % 50),
    joined: new Date(2020, 0, 1 + i).toDateString(),
    bio: `Hello, I'm user ${i}.`,
  })),
];

export const App = () => {
  return (
    <VGrid
      style={{ height: 800, border: "solid 1px #ccc", background: "white" }}
      rows={rows}
      rowHeight={40}
      cols={columns}
      colWidth="width"
      headerRows={1}
    >
      {(row, col) => (
        <div
          style={{
            borderRight: "solid 1px #ccc",
            borderBottom: "solid 1px #ccc",
            background: row === null ? "lightgray" : undefined,
          }}
        >
          {row === null ? col.key : row[col.key]}
        </div>
      )}
    </VGrid>
  );
};

Masonry

For items with different heights laid out in columns such as a gallery, use VMasonry.

import { VMasonry } from "virtua";

const sizes = [100, 180, 140, 220];
const colors = ["skyblue", "pink", "khaki", "lightgreen", "plum"];
const data = Array.from({ length: 1000 }).map((_, i) => ({
  id: i,
  size: sizes[i % 4],
  color: colors[i % 5],
}));

export const App = () => {
  return (
    <VMasonry data={data} lanes={3} gap={8} style={{ height: 800 }}>
      {(item) => (
        <div
          key={item.id}
          style={{
            height: item.size,
            background: item.color,
          }}
        >
          {item.id}
        </div>
      )}
    </VMasonry>
  );
};

More examples

See demo and its source code.

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 are my items squashed, overlapped or rendered inconsistently on resize/add/remove/reorder?

Check that each item has a unique key, such as the id of your data, not its index.

  • React: key of the element of each item
  • Vue: key of the root element in the default slot
  • Solid: the item of data itself, so keep the same reference for the same item
  • Svelte: getKey prop
  • Angular: getKey input

If it still happens, the shift prop may be misused.

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|angular)' or its corresponding type declarations error?

This package uses exports of package.json for entry point of Vue/Solid/Svelte/Angular 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 @tanstack/react-virtual react-virtualized
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 dimensions) βœ… (VGrid) ❌ βœ… (Grid) 🟠 (needs customization) βœ… (Grid)
Tabular data (Columns with headers) βœ… (VGrid) βœ… (TableVirtuoso) βœ… (Supported) 🟠 (needs customization) βœ… (Table)
HTML table element 🟠 (needs customization) βœ… (TableVirtuoso) ❌ 🟠 (needs customization) ❌
Masonry βœ… (VMasonry) βœ… (VirtuosoMasonry) ❌ 🟠 (lanes) βœ… (Masonry)
Window scroller βœ… (WindowVirtualizer) βœ… ❌ βœ… (useWindowVirtualizer) βœ… (WindowScroller)
Dynamic list size βœ… βœ… βœ… βœ… 🟠 (needs AutoSizer)
Dynamic item size βœ… βœ… βœ… (useDynamicRowHeight) βœ… (measureElement) 🟠 (needs CellMeasurer and has wrong destination when scrolling to item imperatively)
Reverse scroll βœ… βœ… ❌ βœ… (anchorTo) ❌
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 βœ… βœ… ❌ βœ… (anchorTo) ❌
Scroll restoration βœ… βœ… (getState) ❌ βœ… (takeSnapshot) ❌
Smooth scroll βœ… βœ… 🟠 (not with useDynamicRowHeight) βœ… ❌
SSR support βœ… (ssrCount) βœ… (initialItemCount) βœ… (defaultHeight) βœ… (initialRect) βœ…
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

Grid features

virtua (VGrid) AG Grid Community MUI X Data Grid react-data-grid @virtuoso.dev/data-table react-window (Grid) @tanstack/react-virtual
Bundle size npm bundle size npm bundle size npm bundle size npm bundle size npm bundle size npm bundle size npm bundle size
Row / column virtualization βœ… βœ… 🟠 (rows are limited to 100 per page, πŸ’° Pro for more) βœ… βœ… βœ… 🟠 (needs customization)
Dynamic row height / column width βœ… (auto) βœ… (autoHeight / autoSizeStrategy) 🟠 (only row height, column width needs autosizing on demand) 🟠 (only column width, measured once) 🟠 (only row height, column width is measured from header cells) ❌ 🟠 (measureElement)
Pinned rows / columns βœ… βœ… πŸ’° (Pro, rows / columns) βœ… 🟠 (only columns) ❌ 🟠 (rangeExtractor)
Row / column spanning βœ… βœ… 🟠 (row spanning needs fixed row height) 🟠 (only columns) 🟠 (only column group headers) ❌ ❌
Sticky group rows βœ… (sectionRows) πŸ’° (Enterprise) ❌ ❌ βœ… (Grouped rows) ❌ 🟠 (rangeExtractor)
SSR support ❌ ❌ ❌ 🟠 (limited to the first few rows) ❌ βœ… (defaultHeight, defaultWidth) βœ… (initialRect)
Window scroller ❌ ❌ ❌ ❌ βœ… (useWindowScroll) ❌ βœ… (useWindowVirtualizer)
Display exceeding browser's max element size limit ❌ βœ… (Stretching) ❌ ❌ ❌ ❌ ❌
Use with TanStack Table βœ… (example) ❌ ❌ ❌ ❌ 🟠 (needs customization) βœ… (guide)
  • βœ… - Built-in supported
  • 🟠 - Supported but partial, limited or requires some user custom code
  • ❌ - Not officially supported
  • πŸ’° - Supported only in paid plans

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.

About

A zero-config, fast and small virtual list, grid and masonry component for React, Vue, Solid, Svelte and Angular.

Topics

Resources

Stars

3.7k stars

Watchers

10 watching

Forks

Releases

Packages

Used by

Contributors

Languages