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 trang web của bạn đáng kể. Công nghệ ảo hoá có thể giúp ích cho bạn!

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

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

Tại sao điều này hữu ích?

Đôi khi, bạn có thể 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 riêng lẻ trên một danh sách như vậy có thể ảnh hưởng đáng kể đến hiệu suất.

Ảo hoá danh sách, hay "tạo cửa sổ" là khái niệm chỉ hiển thị những gì người dùng có thể nhìn thấy. Số lượng phần tử hiển thị đầu tiên 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ị 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 danh sách.

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

Các nút DOM thoát khỏi "cửa sổ" sẽ được tái chế hoặc đượ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. Phương thức này giúp giữ lại số lượng tất cả phần tử được hiển thị cụ thể theo kích thước của cửa sổ.

cửa sổ phản ứng

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. Lớp 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 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.
  • Một hàm kết xuất các hàng được truyền dưới dạng phần tử con của FixedSizeList. Bạn có thể truy cập thông tin chi tiết về 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 hiển thị hàng phải được đính kèm vào phần tử hàng. Các mục trong danh sách được đặt ở vị trí tuyệt đối với giá trị chiều cao và chiều rộng được chỉ định cùng dòng, và tham số style chịu trách nhiệm cho việc này.

Ví dụ về sự cố nhiễu đượ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 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 kích thước cố định, nhưng thay vào đó, bạn sẽ nhận được 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;

Nội dung 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ắp xếp ngẫu nhiên chiều cao của hàng trong ví dụ này. Tuy nhiên, trong một ứng dụng thực tế, sẽ 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ừ API.

Lưới

react-window cũng hỗ trợ việc ảo hoá các 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ổ di chuyển nội dung trong lưới ảo là cửa sổ hai chiều
Việc di chuyển "cửa sổ" nội dung trong lưới ảo là hình hai chiều

Tương tự, bạn có thể sử dụng cả 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 tương tự nhau nhưng thực 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 của hàng bằng cách truyền các hàm thay vì giá trị vào đạo cụ tương ứng.

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

Tải từng phần khi di chuyển

Nhiều trang web cải thiện hiệu suất bằng cách chờ tải và kết xuất các mục trong 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 gần cuối. Mặc dù cách này tốt hơn việc tải tất cả các mục trên danh sách cùng một lúc, nhưng cuối cùng vẫn điền sẵn DOM với hàng nghìn mục nhập hàng nếu người dùng đã cuộn qua nhiều mục đó. Đ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 chậm hơn.

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

Sự khác biệt khi cuộn giữa danh sách thông thường và danh sách ảo hoá
Sự khác biệt khi 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ỏ chứa các phần tử trên trang, nhưng cũng tải từng phần các mục mới hơn khi người dùng cuộn xuống. Gói riêng react-window-infinite-loader giúp bạn thực hiện việc này với react-window.

Hãy xem đoạn mã sau đâ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.

ListComponent hiển thị danh sách có thể trông giống như sau:

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. Đạo cụ đượ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 dữ liệu bổ sung cho danh sách

Đối tượng 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 đối với danh sách ảo hoá.

Việc cuộn xuống danh sách có thể tương tự như vậy, nhưng hiện tại, 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 cuối danh sách. Bạn có thể thực hiện việc này trong khi mỗi lần chỉ hiển thị một "cửa sổ" kết quả duy nhất.

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ể hiển thị cho một mục, tuỳ thuộc vào việc liệu có yêu cầu cho các mục nhập mới hơn hay không 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 không gian trống có thể nhấp nháy nhanh khi các mục mới hơn sắp được 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 cho danh sách ảo, react-window cho phép bạn quét quá mức các mục bằng thuộc tính overscanCount. Thao tác 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 phù hợp với cả thành phần FixedSizeListVariableSizeList, đồng thời 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 quá nhiều thay vì chỉ một mục có thể giúp ngăn chặn hiện tượng nhấp nháy đáng kể của không gian trố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. Điểm mấu chốt của việc sử dụng danh sách ảo hoá là để giảm thiểu số lượng mục nhập mà người dùng có thể nhìn thấy tại bất kỳ thời điểm nào, vì vậy, hãy cố gắng giữ cho 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 dùng thuộc tính overscanColumnsCountoverscanRowsCount để kiểm soát số lượng cột và hàng cần quét vượt mức tương ứng.

Kết luận

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