virtual-lists

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

List Virtualization

列表虚拟化

List virtualization (also known as windowing) is the idea of rendering only visible rows of content in a dynamic list instead of the entire list. The rows rendered are only a small subset of the full list with what is visible (the window) moving as the user scrolls. This can improve rendering performance.
If you use React and need to display large lists of data efficiently, you may be familiar with react-virtualized. It's a windowing library by Brian Vaughn that renders only the items currently visible in a list (within a scrolling "viewport"). This means you don't need to pay the cost of thousands of rows of data being rendered at once.
列表虚拟化(也称为窗口化)是指在动态列表中仅渲染可见行内容,而非整个列表的技术。渲染的行只是完整列表中的一小部分,随着用户滚动,可见区域(窗口)会随之移动。这能够提升渲染性能。
如果你使用React,并且需要高效显示大型数据列表,你可能对react-virtualized并不陌生。它是由Brian Vaughn开发的窗口化库,仅渲染列表中当前可见的项(在滚动“视口”内)。这意味着你无需一次性渲染数千行数据,从而避免额外性能开销。

When to Use

适用场景

  • Use this when rendering large lists or grids (hundreds/thousands of items) that cause performance issues
  • This is helpful for reducing initial render time and improving scroll performance
  • 当渲染大型列表或网格(数百/数千个项)导致性能问题时使用
  • 有助于减少初始渲染时间并提升滚动性能

Instructions

实现步骤

  • Use
    react-window
    (or
    react-virtualized
    ) to render only visible items in a scrollable container
  • Choose
    FixedSizeList
    for items of equal height or
    VariableSizeList
    for items of different heights
  • Use
    react-window-infinite-loader
    for incrementally loading data as the user scrolls
  • Consider CSS
    content-visibility: auto
    for simpler cases where full virtualization isn't needed
  • 使用
    react-window
    (或
    react-virtualized
    )在可滚动容器中仅渲染可见项
  • 若列表项高度相同,选择
    FixedSizeList
    ;若高度不同,选择
    VariableSizeList
  • 使用
    react-window-infinite-loader
    实现用户滚动时增量加载数据
  • 若无需完整虚拟化,可考虑使用CSS属性
    content-visibility: auto
    来处理简单场景

Details

详细说明

How does list virtualization work?

列表虚拟化的工作原理

"Virtualizing" a list of items involves maintaining a window and moving that window around your list. Windowing in react-virtualized works by:
  • Having a small container DOM element (e.g
    <ul>
    ) with relative positioning (window)
  • Having a big DOM element for scrolling
  • Absolutely positioning children inside the container, setting their styles for top, left, width and height.
Rather than rendering 1000s of elements from a list at once (which can cause slower initial rendering or impact scroll performance), virtualization focuses on rendering just items visible to the user.
This can help keep list rendering fast on mid to low-end devices. You can fetch/display more items as the user scrolls, unloading previous entries and replacing them with new ones.
对列表项进行“虚拟化”需要维护一个窗口在列表中移动该窗口。react-virtualized中的窗口化通过以下方式实现:
  • 创建一个小的相对定位容器DOM元素(例如
    <ul>
    )作为窗口
  • 创建一个用于滚动的大DOM元素
  • 将子元素绝对定位在容器内,设置其top、left、width和height样式
虚拟化技术不会一次性渲染列表中的数千个元素(这会导致初始渲染变慢或影响滚动性能),而是专注于仅渲染用户可见的项
这有助于在中低端设备上保持列表渲染的流畅性。你可以在用户滚动时获取/显示更多项,同时卸载之前的条目并替换为新条目。

A smaller alternative to react-virtualized

react-virtualized的轻量替代方案

react-window is a rewrite of react-virtualized by the same author aiming to be smaller, faster and more tree-shakeable.
In a tree-shakeable library, size is a function of which API surfaces you choose to use. You can see ~20-30KB (gzipped) savings using it in place of react-virtualized.
The APIs for both packages are similar and where they differ, react-window tends to be simpler. react-window's components include:
react-window是同一作者对react-virtualized的重写版本,旨在更轻量、更快且更支持tree-shakeable(摇树优化)
在支持摇树优化的库中,体积大小取决于你使用的API范围。与react-virtualized相比,使用react-window可节省约20-30KB(gzip压缩后)的体积。
两个库的API相似,不同之处在于react-window的API更简洁。react-window的组件包括:

List

List

Lists render a windowed list (row) of elements meaning that only the visible rows are displayed to users (e.g FixedSizeList, VariableSizeList). Lists use a Grid (internally) to render rows, relaying props to that inner Grid.
Rendering a list of data using React
Here's an example of rendering a list of simple data (
itemsArray
) using React:
js
import React from "react";
import ReactDOM from "react-dom";

const itemsArray = [
  { name: "Drake" },
  { name: "Halsey" },
  { name: "Camillo Cabello" },
  { name: "Travis Scott" },
  { name: "Bazzi" },
  { name: "Flume" },
  { name: "Nicki Minaj" },
  { name: "Kodak Black" },
  { name: "Tyga" },
  { name: "Buno Mars" },
  { name: "Lil Wayne" }, ...
]; // our data

const Row = ({ index, style }) => (
  <div className={index % 2 ? "ListItemOdd" : "ListItemEven"} style={style}>
    {itemsArray[index].name}
  </div>
);

const Example = () => (
  <div
    style={{
      height: 150,
      width: 300
    }}
    class="List"
  >
    {itemsArray.map((item, index) => Row({ index }))}
  </div>
);

ReactDOM.render(<Example />, document.getElementById("root"));
Rendering a list using react-window
...and here's the same example using react-window's
FixedSizeList
, which takes a few props (
width
,
height
,
itemCount
,
itemSize
) and a row rendering function passed as a child:
js
import React from "react";
import ReactDOM from "react-dom";
import { FixedSizeList as List } from "react-window";

const itemsArray = [...]; // our data

const Row = ({ index, style }) => (
  <div className={index % 2 ? "ListItemOdd" : "ListItemEven"} style={style}>
    {itemsArray[index].name}
  </div>
);

const Example = () => (
  <List
    className="List"
    height={150}
    itemCount={itemsArray.length}
    itemSize={35}
    width={300}
  >
    {Row}
  </List>
);

ReactDOM.render(<Example />, document.getElementById("root"));
You can try out
FixedSizeList
on CodeSandbox.
List组件用于渲染窗口化的列表行元素,即仅向用户显示可见行(例如FixedSizeListVariableSizeList)。List内部使用Grid组件渲染行,并将属性传递给内部的Grid。
使用React渲染数据列表
以下是使用React渲染简单数据列表(
itemsArray
)的示例:
js
import React from "react";
import ReactDOM from "react-dom";

const itemsArray = [
  { name: "Drake" },
  { name: "Halsey" },
  { name: "Camillo Cabello" },
  { name: "Travis Scott" },
  { name: "Bazzi" },
  { name: "Flume" },
  { name: "Nicki Minaj" },
  { name: "Kodak Black" },
  { name: "Tyga" },
  { name: "Buno Mars" },
  { name: "Lil Wayne" }, ...
]; // 数据源

const Row = ({ index, style }) => (
  <div className={index % 2 ? "ListItemOdd" : "ListItemEven"} style={style}>
    {itemsArray[index].name}
  </div>
);

const Example = () => (
  <div
    style={{
      height: 150,
      width: 300
    }}
    class="List"
  >
    {itemsArray.map((item, index) => Row({ index }))}
  </div>
);

ReactDOM.render(<Example />, document.getElementById("root"));
使用react-window渲染列表
以下是使用react-window的
FixedSizeList
实现相同功能的示例,该组件接受几个属性(
width
height
itemCount
itemSize
),并将行渲染函数作为子组件传入:
js
import React from "react";
import ReactDOM from "react-dom";
import { FixedSizeList as List } from "react-window";

const itemsArray = [...]; // 数据源

const Row = ({ index, style }) => (
  <div className={index % 2 ? "ListItemOdd" : "ListItemEven"} style={style}>
    {itemsArray[index].name}
  </div>
);

const Example = () => (
  <List
    className="List"
    height={150}
    itemCount={itemsArray.length}
    itemSize={35}
    width={300}
  >
    {Row}
  </List>
);

ReactDOM.render(<Example />, document.getElementById("root"));
你可以在CodeSandbox上尝试
FixedSizeList

Grid

Grid

Grid renders tabular data with virtualization along the vertical and horizontal axes (e.g FixedSizeGrid, VariableSizeGrid). It only renders the Grid cells needed to fill itself based on current horizontal/vertical scroll positions.
js
import React from 'react';
import ReactDOM from 'react-dom';
import { FixedSizeGrid as Grid } from 'react-window';

const itemsArray = [
  [{},{},{},...],
  [{},{},{},...],
  [{},{},{},...],
  [{},{},{},...],
];

const Cell = ({ columnIndex, rowIndex, style }) => (
  <div
    className={
      columnIndex % 2
        ? rowIndex % 2 === 0
          ? 'GridItemOdd'
          : 'GridItemEven'
        : rowIndex % 2
          ? 'GridItemOdd'
          : 'GridItemEven'
    }
    style={style}
  >
    {itemsArray[rowIndex][columnIndex].name}
  </div>
);

const Example = () => (
  <Grid
    className="Grid"
    columnCount={5}
    columnWidth={100}
    height={150}
    rowCount={5}
    rowHeight={35}
    width={300}
  >
    {Cell}
  </Grid>
);

ReactDOM.render(<Example />, document.getElementById('root'));
You can also try out
FixedSizeGrid
on CodeSandbox.
Grid组件用于渲染表格数据,并在垂直和水平方向上实现虚拟化(例如FixedSizeGridVariableSizeGrid)。它仅根据当前水平/垂直滚动位置渲染填充自身所需的网格单元格。
js
import React from 'react';
import ReactDOM from 'react-dom';
import { FixedSizeGrid as Grid } from 'react-window';

const itemsArray = [
  [{},{},{},...],
  [{},{},{},...],
  [{},{},{},...],
  [{},{},{},...],
];

const Cell = ({ columnIndex, rowIndex, style }) => (
  <div
    className={
      columnIndex % 2
        ? rowIndex % 2 === 0
          ? 'GridItemOdd'
          : 'GridItemEven'
        : rowIndex % 2
          ? 'GridItemOdd'
          : 'GridItemEven'
    }
    style={style}
  >
    {itemsArray[rowIndex][columnIndex].name}
  </div>
);

const Example = () => (
  <Grid
    className="Grid"
    columnCount={5}
    columnWidth={100}
    height={150}
    rowCount={5}
    rowHeight={35}
    width={300}
  >
    {Cell}
  </Grid>
);

ReactDOM.render(<Example />, document.getElementById('root'));
你也可以在CodeSandbox上尝试
FixedSizeGrid

More in-depth react-window examples

更多react-window深度示例

Scott Taylor implemented an open-source Pitchfork music reviews scraper (src) using
react-window
and
FixedSizeGrid
.
Pitchfork scraper uses react-window-infinite-loader (demo) which helps break large data sets down into chunks that can be loaded as they are scrolled into view.
Here's a snippet of how react-window-infinite-loader is incorporated in this app:
js
import React, { Component } from 'react';
import { FixedSizeGrid as Grid } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader';

// ...

render() {
  return (
    <InfiniteLoader
      isItemLoaded={this.isItemLoaded}
      loadMoreItems={this.loadMoreItems}
      itemCount={this.state.count + 1}
    >
      {({ onItemsRendered, ref }) => (
        <Grid
          onItemsRendered={this.onItemsRendered(onItemsRendered)}
          columnCount={COLUMN_SIZE}
          columnWidth={180}
          height={800}
          rowCount={Math.max(this.state.count / COLUMN_SIZE)}
          rowHeight={220}
          width={1024}
          ref={ref}
        >
          {this.renderCell}
        </Grid>
      )}
    </InfiniteLoader>
  );
}
You might find the commit porting the app over from
react-virtualized
useful.
An implementation using
FixedSizeList
is also available:
js
return (
  <InfiniteLoader
    isItemLoaded={this.isItemLoaded}
    loadMoreItems={this.loadMoreItems}
    itemCount={this.state.count}
  >
    {({ onItemsRendered, ref }) => (
      <section>
        <FixedSizeList
          itemCount={this.state.count}
          itemSize={ROW_HEIGHT}
          onItemsRendered={onItemsRendered}
          height={this.state.height}
          width={this.state.width}
          ref={ref}
        >
          {this.renderCell}
        </FixedSizeList>
      </section>
    )}
  </InfiniteLoader>
);
For even more complex needs, a The Movie Database demo app used react-virtualized and Infinite Loader under the hood. Porting it over to react-window and react-window-infinite-loader didn't take long, but we did discover a few components were not yet supported. Regardless, the final functionality is pretty close.
The missing components were WindowScroller and AutoSizer, which we'll look at next.
js
// ...
return (
  <section>
    <AutoSizer disableHeight>
      {({width}) => {
        const {movies, hasMore} = this.props;
        const rowCount = getRowsAmount(width, movies.length, hasMore);
        // ...
        return (
          <InfiniteLoader
            ref={this.infiniteLoaderRef}
            // ...
            {({onRowsRendered, registerChild}) => (
              <WindowScroller>
                {({height, scrollTop}) => (
Scott Taylor使用
react-window
FixedSizeGrid
实现了一个开源的Pitchfork音乐评论爬虫源码)。
该爬虫使用了react-window-infinite-loader演示),它有助于将大型数据集拆分为多个块,当用户滚动到对应区域时再加载这些块。
以下是该应用中集成react-window-infinite-loader的代码片段:
js
import React, { Component } from 'react';
import { FixedSizeGrid as Grid } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader';

// ...

render() {
  return (
    <InfiniteLoader
      isItemLoaded={this.isItemLoaded}
      loadMoreItems={this.loadMoreItems}
      itemCount={this.state.count + 1}
    >
      {({ onItemsRendered, ref }) => (
        <Grid
          onItemsRendered={this.onItemsRendered(onItemsRendered)}
          columnCount={COLUMN_SIZE}
          columnWidth={180}
          height={800}
          rowCount={Math.max(this.state.count / COLUMN_SIZE)}
          rowHeight={220}
          width={1024}
          ref={ref}
        >
          {this.renderCell}
        </Grid>
      )}
    </InfiniteLoader>
  );
}
你可以查看将应用从
react-virtualized
迁移过来的提交记录,这会对你有所帮助。
还有一个使用
FixedSizeList
的实现示例:
js
return (
  <InfiniteLoader
    isItemLoaded={this.isItemLoaded}
    loadMoreItems={this.loadMoreItems}
    itemCount={this.state.count}
  >
    {({ onItemsRendered, ref }) => (
      <section>
        <FixedSizeList
          itemCount={this.state.count}
          itemSize={ROW_HEIGHT}
          onItemsRendered={onItemsRendered}
          height={this.state.height}
          width={this.state.width}
          ref={ref}
        >
          {this.renderCell}
        </FixedSizeList>
      </section>
    )}
  </InfiniteLoader>
);
对于更复杂的需求,有一个基于The Movie Database演示应用,它底层使用了react-virtualized和Infinite Loader。将其迁移到react-window和react-window-infinite-loader并不需要太长时间,但我们发现有一些组件尚未被支持。不过最终实现的功能与原应用非常接近
缺失的组件是WindowScroller和AutoSizer,我们接下来会介绍它们。
js
// ...
return (
  <section>
    <AutoSizer disableHeight>
      {({width}) => {
        const {movies, hasMore} = this.props;
        const rowCount = getRowsAmount(width, movies.length, hasMore);
        // ...
        return (
          <InfiniteLoader
            ref={this.infiniteLoaderRef}
            // ...
            {({onRowsRendered, registerChild}) => (
              <WindowScroller>
                {({height, scrollTop}) => (

What's missing from react-window?

react-window缺失的功能

react-window does not yet have the complete API surface of react-virtualized, so do check the comparison docs if considering it. What's missing?
  • WindowScroller - This is a
    react-virtualized
    component that enables Lists to be scrolled based on the window's scroll positions. There are currently no plans to implement this for react-window so you'll need to solve this in userland.
  • AutoSizer - HOC that grows to fit all of the available space, automatically adjusting the width and height of a single child. Brian implemented this as a standalone package. Follow this issue for the latest.
  • CellMeasurer - HOC automatically measuring a cell's content by rendering it in a way that is not visible to the user. Follow here for discussion on support.
That said, we found react-window sufficient for most of our needs with what it includes out of the box.
react-window目前的API覆盖范围不如react-virtualized完整,因此在考虑使用它时,请查看对比文档。缺失的功能包括:
  • WindowScroller - 这是react-virtualized的一个组件,允许列表根据窗口的滚动位置进行滚动。目前暂无为react-window实现该组件的计划,你需要自行实现相关逻辑。
  • AutoSizer - 一个高阶组件,可自适应可用空间,自动调整单个子组件的宽度和高度。Brian将其作为一个独立包发布。你可以关注这个议题获取最新动态。
  • CellMeasurer - 一个高阶组件,通过在不可见区域渲染单元格内容来自动测量其尺寸。你可以关注这里了解相关支持情况。
尽管如此,我们发现react-window自带的功能已经能满足大多数需求。

Improvements in the web platform

Web平台的改进

Some modern browsers now support CSS content-visibility.
content-visibility:auto
allows you to skip rendering & painting offscreen content until needed. If you have a long HTML document with costly rendering, consider trying the property out.
For rendering lists of dynamic content, I still recommend using a library like react-window. It would be hard to have a
content-visibility:hidden
version of such a library that beats a version aggressively using
display:none
or removing DOM nodes when offscreen like many list virtualization libraries may do today.
一些现代浏览器现在支持CSS content-visibility属性。
content-visibility:auto
允许你跳过渲染和绘制屏幕外的内容,直到需要时再进行。如果你的长HTML文档渲染成本较高,可以尝试使用该属性。
对于动态内容列表的渲染,我仍然推荐使用react-window这样的库。目前很难实现一个基于
content-visibility:hidden
的库,其性能能够超过像许多列表虚拟化库那样使用
display:none
或在元素离开屏幕时移除DOM节点的实现。

Further reading

扩展阅读

Source

来源