Trực quan hoá danh sách lớn bằng cửa sổ phản ứng

Các bảng và danh sách quá lớn có thể làm chậm hiệu suất của trang web một cách đáng kể. Ảo hoá có thể giúp bạn!

react-window là một thư viện cho phép hiển thị hiệu quả các danh sách lớn.

Dưới đây là ví dụ về một danh sách chứa 1000 hàng được hiển thị bằng react-window. Hãy thử cuộn nhanh nhất có thể.

Vì sao tính năng này hữu ích?

Có thể đôi khi bạn cần hiển thị một bảng hoặc danh sách lớn chứa nhiều hàng. Việc tải từng mục trên danh sách như vậy có thể ảnh hưởng đáng kể đến hiệu suất.

Ảo hoá danh sách (list virtualization) hay còn gọi là "cửa sổ" (windowing) là khái niệm chỉ hiển thị những nội dung mà người dùng nhìn thấy. Số lượng phần tử được kết xuất lúc đầu là một tập hợp con rất nhỏ của toàn bộ danh sách và "cửa sổ" của nội dung hiển thị sẽ di chuyển khi người dùng tiếp tục cuộn. Điều này giúp cải thiện cả hiệu suất kết xuất và cuộn của danh sách.

Cửa sổ nội dung trong danh sách ảo
Di chuyển "cửa sổ" nội dung trong danh sách ảo hoá

Các nút DOM thoát khỏi "cửa sổ" sẽ được tái chế hoặc ngay lập tức được thay thế bằng các phần tử mới hơn khi người dùng cuộn xuống danh sách. Điều này giúp số lượng tất cả các phần tử được kết xuất cụ thể theo kích thước của cửa sổ.

react-window

react-window là một thư viện nhỏ của bên thứ ba giúp bạn dễ dàng tạo danh sách ảo hoá trong ứng dụng. Thư viện này cung cấp một số API cơ sở có thể dùng cho nhiều loại danh sách và bảng.

Trường hợp nên sử dụng danh sách có kích thước cố định

Sử dụng thành phần FixedSizeList nếu bạn có một danh sách dài, một chiều gồm các mục có kích thước bằng nhau.

import React from 'react';
import { FixedSizeList } from 'react-window';

const items = [...] // some list of items

const Row = ({ index, style }) => (
  <div style={style}>
     {/* define the row component using items[index] */}
  </div>
);

const ListComponent = () => (
  <FixedSizeList
    height={500}
    width={500}
    itemSize={120}
    itemCount={items.length}
  >
    {Row}
  </FixedSizeList>
);

export default ListComponent;
  • Thành phần FixedSizeList chấp nhận thuộc tính height, widthitemSize để kiểm soát kích thước của các mục trong danh sách.
  • Hàm hiển thị các hàng được truyền dưới dạng phần tử con đến FixedSizeList. Bạn có thể truy cập thông tin chi tiết về một mục cụ thể bằng đối số index (items[index]).
  • Tham số style cũng được truyền vào phương thức kết xuất hàng mà phải được đính kèm vào phần tử hàng. Các mục trong danh sách được định vị tuyệt đối với các giá trị chiều cao và chiều rộng được chỉ định cùng dòng và thông số style chịu trách nhiệm cho việc này.

Ví dụ về Glitch được trình bày ở phần trước của bài viết này là một ví dụ về thành phần FixedSizeList.

Trường hợp nên sử dụng danh sách có kích thước biến đổi

Sử dụng thành phần VariableSizeList để hiển thị danh sách các mục có kích thước khác nhau. Thành phần này hoạt động giống như danh sách có kích thước cố định, nhưng yêu cầu một hàm cho thuộc tính itemSize thay vì một giá trị cụ thể.

import React from 'react';
import { VariableSizeList } from 'react-window';

const items = [...] // some list of items

const Row = ({ index, style }) => (
  <div style={style}>
     {/* define the row component using items[index] */}
  </div>
);

const getItemSize = index => {
  // return a size for items[index]
}

const ListComponent = () => (
  <VariableSizeList
    height={500}
    width={500}
    itemCount={items.length}
    itemSize={getItemSize}
  >
    {Row}
  </VariableSizeList>
);

export default ListComponent;

Phần nhúng sau đây cho thấy ví dụ về thành phần này.

Hàm kích thước mặt hàng được truyền đến thuộc tính itemSize sẽ tạo ngẫu nhiên chiều cao hàng trong ví dụ này. Tuy nhiên, trong một ứng dụng thực tế, phải có logic thực tế xác định kích thước của từng mục. Tốt nhất là bạn nên tính toán các kích thước này dựa trên dữ liệu hoặc lấy từ một API.

Lưới

react-window cũng hỗ trợ ảo hoá danh sách hoặc lưới đa chiều. Trong ngữ cảnh này, "cửa sổ" của nội dung hiển thị sẽ thay đổi khi người dùng cuộn theo chiều ngang chiều dọc.

Cửa sổ nội dung di chuyển trong lưới ảo là hai chiều
Di chuyển "cửa sổ" nội dung trong lưới ảo hoá là hai chiều

Tương tự, bạn có thể sử dụng cả hai thành phần FixedSizeGridVariableSizeGrid, tuỳ thuộc vào việc kích thước của các mục cụ thể trong danh sách có thể khác nhau hay không.

  • Đối với FixedSizeGrid, API cũng tương tự nhưng cần thể hiện chiều cao, chiều rộng và số lượng mục cho cả cột và hàng.
  • Đối với VariableSizeGrid, bạn có thể thay đổi cả chiều rộng cột và chiều cao hàng bằng cách truyền vào các hàm thay vì giá trị cho các thuộc tính tương ứng.

Hãy xem tài liệu để xem các ví dụ về lưới ảo hoá.

Tải từng phần khi cuộn

Nhiều trang web cải thiện hiệu suất bằng cách chờ tải và hiển thị các mục trong một danh sách dài cho đến khi người dùng cuộn xuống. Kỹ thuật này, thường được gọi là "tải vô hạn", thêm các nút DOM mới vào danh sách khi người dùng cuộn qua một ngưỡng nhất định khi gần đến cuối. Mặc dù cách này tốt hơn so với việc tải tất cả các mục trên một danh sách cùng một lúc, nhưng cuối cùng vẫn điền DOM bằng hàng nghìn mục nhập hàng nếu người dùng đã cuộn qua nhiều mục như vậy. Điều này có thể dẫn đến kích thước DOM quá lớn, bắt đầu ảnh hưởng đến hiệu suất bằng cách làm cho việc tính toán kiểu và đột biến DOM trở nên chậm hơn.

Sơ đồ sau đây có thể giúp tóm tắt điều này:

Sự khác biệt về việc cuộn giữa danh sách thông thường và danh sách ảo hoá
Sự khác biệt về việc cuộn giữa danh sách thông thường và danh sách ảo hoá

Cách tốt nhất để giải quyết vấn đề này là tiếp tục sử dụng một thư viện như react-window để duy trì một "cửa sổ" nhỏ gồm các phần tử trên một trang, nhưng cũng tải từng phần mới hơn khi người dùng cuộn xuống. Một gói riêng biệt, react-window-infinite-loader, giúp bạn có thể thực hiện việc này với react-window.

Hãy xem xét đoạn mã sau đây, trong đó cho thấy ví dụ về trạng thái được quản lý trong thành phần App mẹ.

import React, { Component } from 'react';

import ListComponent from './ListComponent';

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      items: [], // instantiate initial list here
      moreItemsLoading: false,
      hasNextPage: true
    };

    this.loadMore = this.loadMore.bind(this);
  }

  loadMore() {
   // method to fetch newer entries for the list
  }

  render() {
    const { items, moreItemsLoading, hasNextPage } = this.state;

    return (
      <ListComponent
        items={items}
        moreItemsLoading={moreItemsLoading}
        loadMore={this.loadMore}
        hasNextPage={hasNextPage}
      />
    );
  }
}

export default App;

Phương thức loadMore được truyền vào ListComponent con chứa danh sách trình tải vô hạn. Điều này rất quan trọng vì trình tải vô hạn cần kích hoạt lệnh gọi lại để tải thêm các mục sau khi người dùng cuộn qua một điểm nhất định.

Dưới đây là cách ListComponent hiển thị danh sách:

import React from 'react';
import { FixedSizeList } from 'react-window';
import InfiniteLoader from "react-window-infinite-loader";

const ListComponent = ({ items, moreItemsLoading, loadMore, hasNextPage }) => {
  const Row = ({ index, style }) => (
     {/* define the row component using items[index] */}
  );

  const itemCount = hasNextPage ? items.length + 1 : items.length;

  return (
    <InfiniteLoader
      isItemLoaded={index => index < items.length}
      itemCount={itemCount}
      loadMoreItems={loadMore}
    >
      {({ onItemsRendered, ref }) => (
        <FixedSizeList
          height={500}
          width={500}
          itemCount={itemCount}
          itemSize={120}
          onItemsRendered={onItemsRendered}
          ref={ref}
        >
          {Row}
        </FixedSizeList>
      )}
  </InfiniteLoader>
  )
};

export default ListComponent;

Ở đây, thành phần FixedSizeList được gói trong InfiniteLoader. Các thuộc tính được chỉ định cho trình tải là:

  • isItemLoaded: Phương thức kiểm tra xem một mục nhất định đã tải hay chưa
  • itemCount: Số mục trong danh sách (hoặc dự kiến)
  • loadMoreItems: Lệnh gọi lại trả về một lời hứa phân giải thành dữ liệu bổ sung cho danh sách

Đoạn mã kết xuất dùng để trả về một hàm mà thành phần danh sách dùng để kết xuất. Cả hai thuộc tính onItemsRenderedref đều là các thuộc tính cần được truyền vào.

Sau đây là ví dụ về cách hoạt động của tính năng tải vô hạn với danh sách được ảo hoá.

Việc cuộn xuống danh sách có thể vẫn như cũ, nhưng giờ đây, một yêu cầu sẽ được thực hiện để truy xuất 10 người dùng từ API người dùng ngẫu nhiên mỗi khi bạn cuộn gần đến cuối danh sách. Việc này được thực hiện trong khi chỉ hiển thị một "cửa sổ" kết quả tại một thời điểm.

Bằng cách kiểm tra index của một mục nhất định, trạng thái tải khác có thể được hiển thị cho một mục tuỳ thuộc vào việc liệu một yêu cầu đã được thực hiện cho các mục mới hơn hay chưa và mục đó vẫn đang tải.

Ví dụ:

const Row = ({ index, style }) => {
  const itemLoading = index === items.length;

  if (itemLoading) {
      // return loading state
  } else {
      // return item
  }
};

Quét quá mức

Vì các mục trong danh sách ảo hoá chỉ thay đổi khi người dùng cuộn, nên khoảng trống có thể nhấp nháy trong giây lát khi các mục mới hơn sắp hiển thị. Bạn có thể thử cuộn nhanh bất kỳ ví dụ nào trước đó trong hướng dẫn này để nhận thấy điều này.

Để cải thiện trải nghiệm người dùng của danh sách ảo hoá, react-window cho phép bạn quét nhiều lần các mục bằng thuộc tính overscanCount. Điều này cho phép bạn xác định số lượng mục bên ngoài "cửa sổ" hiển thị để hiển thị mọi lúc.

<FixedSizeList
  //...
  overscanCount={4}
>
  {...}
</FixedSizeList>

overscanCount hoạt động cho cả thành phần FixedSizeListVariableSizeList và có giá trị mặc định là 1. Tuỳ thuộc vào kích thước của danh sách cũng như kích thước của từng mục, việc quét nhiều lần không chỉ một mục có thể giúp ngăn chặn tình trạng không gian trống xuất hiện chớp nhoáng khi người dùng cuộn. Tuy nhiên, việc quét quá nhiều mục nhập có thể ảnh hưởng tiêu cực đến hiệu suất. Mục đích của việc sử dụng danh sách ảo hoá là giảm thiểu số mục mà người dùng có thể thấy tại một thời điểm bất kỳ, vì vậy, hãy cố gắng giảm thiểu số mục được quét quá mức.

Đối với FixedSizeGridVariableSizeGrid, hãy dùng thuộc tính overscanColumnsCountoverscanRowsCount để kiểm soát số lượng cột và hàng cần quét quá mức tương ứng.

Kết luận

Nếu bạn không chắc chắn vị trí để bắt đầu ảo hoá các danh sách và bảng trong ứng dụng, hãy làm theo các bước sau:

  1. Đo lường hiệu suất kết xuất và cuộn. Bài viết này cho biết cách sử dụng đồng hồ FPS trong Công cụ của Chrome cho nhà phát triển để khám phá mức độ hiệu quả của việc hiển thị các mục trên danh sách.
  2. Đưa react-window vào mọi danh sách hoặc lưới dài đang ảnh hưởng đến hiệu suất.
  3. Nếu có một số tính năng không được hỗ trợ trong react-window, hãy cân nhắc sử dụng react-virtualized nếu bạn không thể tự thêm chức năng này.
  4. Gói danh sách ảo hoá của bạn bằng react-window-infinite-loader nếu bạn cần tải lười các mục khi người dùng cuộn.
  5. Sử dụng thuộc tính overscanCount cho danh sách và thuộc tính overscanColumnsCountoverscanRowsCount cho lưới để ngăn nội dung trống xuất hiện. Đừng quét quá nhiều mục nhập vì điều này sẽ ảnh hưởng tiêu cực đến hiệu suất.