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 siêu lớn có thể làm giảm đáng kể hiệu suất của trang web. Tính năng ả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 1.000 hàng đang được kết xuất bằng react-window. Hãy thử di chuyển nhanh nhất có thể.

Tại sao thông tin này hữu ích?

Đô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 hay "cửa sổ hoá" là khái niệm chỉ kết xuất những gì người dùng nhìn thấy. Số lượng phần tử được hiển thị ban đầu chỉ là một tập hợp con rất nhỏ của toàn bộ danh sách và "cửa sổ" 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 hoá
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 thay thế ngay 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 phù hợp với 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 trong ứng dụng. Nó cung cấp một số API cơ bản có thể dùng cho nhiều loại danh sách và bảng.

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

Hãy 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 một thuộc tính height, widthitemSize để kiểm soát kích thước của các mục trong danh sách.
  • Một hàm kết xuất các hàng được truyền dưới dạng thành phần 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 nội tuyến, đồng thời tham số style chịu trách nhiệm cho việc này.

Ví dụ về Glitch được trình bày trước đó trong bài viết này cho thấy 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 thay đổ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 theo cách tương tự như danh sách có kích thước cố định, nhưng thay vào đó, thành phần này mong đợi 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 một ví dụ về thành phần này.

Hàm kích thước mục được truyền đến thuộc tính itemSize sẽ ngẫu nhiên hoá chiều cao hàng trong ví dụ này. Tuy nhiên, trong một ứng dụng thực tế, cần 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á các danh sách hoặc lưới nhiều chiều. Trong trường hợp này, "cửa sổ" 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 hoá là hai chiều
"Cửa sổ" nội dung di chuyển trong lưới ảo hoá là hai chiều

Tương tự, bạn có thể 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ể thay đổi hay không.

  • Đối với FixedSizeGrid, API này gần giống nhau nhưng có điểm khác biệt là chiều cao, chiều rộng và số lượng mục cần được biểu thị 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 các hàm thay vì giá trị vào các thuộc tính tương ứng.

Hãy xem tài liệu để xem 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") sẽ 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 gầ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 trong danh sách cùng một lúc, nhưng cách này vẫn sẽ điền vào DOM hàng nghìn mục 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 các phép tính về kiểu và đột biến DOM chậm hơn.

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

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

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 các mục mới hơn một cách trì hoã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 thực hiện việc này bằng react-window.

Hãy xem xét đoạn mã sau đây cho thấy một 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 một 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 mộ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.

Sau đâ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;

Trong đó, thành phần FixedSizeList được bao bọc trong InfiniteLoader. Các prop được chỉ định cho trình tải là:

  • isItemLoaded: Phương thức kiểm tra xem một mục cụ thể đã tải hay chưa
  • itemCount: Số lượng 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

Render prop được 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à những thuộc tính cần được truyền vào.

Sau đây là ví dụ về cách tải vô hạn có thể hoạt động với danh sách ảo hoá.

Việc cuộn xuống danh sách có thể không có gì khác biệt, nhưng giờ đây, một yêu cầu sẽ được gửi để 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. Tất cả những việc này được thực hiện trong khi chỉ kết xuất 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, bạn có thể cho thấy một trạng thái tải khác cho một mục, tuỳ thuộc vào việc yêu cầu đã được thực hiện cho các mục mới hơn và mục đó vẫn đang tải hay không.

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 chỉ thay đổi khi người dùng cuộn, nên khoảng trống có thể nhấp nháy trong thời gian ngắn khi các mục mới hơn sắp xuất hiện. 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 các danh sách ảo hoá, react-window cho phép bạn quét quá mức 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ị để kết xuất mọi lúc.

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

overscanCount hoạt động cho cả thành phần FixedSizeListVariableSizeList, đồng thời có giá trị mặc định là 1. Tuỳ thuộc vào độ lớn của danh sách cũng như kích thước của từng mục, việc quét quá mức nhiều hơn một mục có thể giúp ngăn hiện tượng nhấp nháy đáng chú ý của khoảng trống khi người dùng cuộn. Tuy nhiên, việc quét quá nhiều mục 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ố lượng 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ữ số lượng mục được quét quá mức ở mức thấp nhất có thể.

Đối với FixedSizeGridVariableSizeGrid, hãy sử dụng các 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 biết nên bắt đầu từ đâu để ảo hoá 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 trình bày cách sử dụng đồng hồ đo FPS trong Công cụ cho nhà phát triển của Chrome để khám phá mức độ hiệu quả của việc kết xuất các mục trong danh sách.
  2. Thêm react-window cho 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. Bọc danh sách ảo hoá bằng react-window-infinite-loader nếu bạn cần tải các mục một cách linh hoạt khi người dùng cuộn.
  5. Hãy dùng thuộc tính overscanCount cho danh sách và thuộc tính overscanColumnsCount cũng như overscanRowsCount cho lưới để ngăn nội dung trống xuất hiện chớp nhoáng. Không quét quá nhiều mục vì điều này sẽ ảnh hưởng tiêu cực đến hiệu suất.