จำลองรายการขนาดใหญ่ด้วยหน้าต่างแสดงปฏิกิริยา

ตารางและรายการขนาดใหญ่มากอาจทำให้ประสิทธิภาพของเว็บไซต์ช้าลงอย่างมาก ระบบเสมือนจริงช่วยคุณได้

react-window เป็นห้องสมุดที่ช่วยแสดงผลรายการขนาดใหญ่ได้อย่างมีประสิทธิภาพ

ต่อไปนี้คือตัวอย่างรายการที่มี 1,000 แถวที่แสดงผลด้วย react-window ลองเลื่อนให้เร็วสุดเท่าที่ทำได้

เหตุใดจึงมีประโยชน์

บางครั้งคุณอาจต้องแสดงตารางหรือรายการขนาดใหญ่ที่มีแถวจํานวนมาก การโหลดสินค้าทุกรายการในรายการดังกล่าวอาจส่งผลต่อประสิทธิภาพอย่างมาก

การจําลองรายการหรือ "การแบ่งหน้าจอ" เป็นแนวคิดในการแสดงผลเฉพาะสิ่งที่ผู้ใช้มองเห็น จํานวนองค์ประกอบที่แสดงผลในตอนแรกคือชุดย่อยเล็กๆ ของรายการทั้งหมด และ "กรอบ" ของเนื้อหาที่มองเห็นได้จะเลื่อนเมื่อผู้ใช้เลื่อนต่อไป ซึ่งจะช่วยปรับปรุงทั้งประสิทธิภาพการแสดงผลและการเลื่อนของรายการ

หน้าต่างของเนื้อหาในรายการเสมือนจริง
การย้าย "กรอบเวลา" ของเนื้อหาในรายการเสมือนจริง

ระบบจะรีไซเคิลโหนด DOM ที่ออกจาก "กรอบเวลา" หรือแทนที่ด้วยองค์ประกอบใหม่ทันทีเมื่อผู้ใช้เลื่อนรายการลง ซึ่งช่วยรักษาจำนวนองค์ประกอบที่แสดงผลทั้งหมดตามขนาดของหน้าต่างไว้โดยเฉพาะ

react-window

react-window เป็นไลบรารีขนาดเล็กของบุคคลที่สามที่ช่วยให้คุณสร้างรายการเสมือนในแอปพลิเคชันได้ง่ายขึ้น ซึ่งให้บริการ API พื้นฐานจํานวนหนึ่งที่ใช้กับรายการและตารางประเภทต่างๆ ได้

กรณีที่ควรใช้รายการขนาดคงที่

ใช้คอมโพเนนต์ FixedSizeList หากคุณมีรายการขนาดเท่าๆ กันแบบ 1 มิติที่ยาว

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;
  • คอมโพเนนต์ FixedSizeList ยอมรับพร็อพเพอร์ตี้ height, width และ itemSize เพื่อควบคุมขนาดของรายการภายในรายการ
  • ฟังก์ชันที่แสดงผลแถวต่างๆ จะส่งผ่านเป็นหน่วยย่อยไปยัง FixedSizeList รายละเอียดเกี่ยวกับรายการหนึ่งๆ จะเข้าถึงได้ด้วยอาร์กิวเมนต์ index (items[index])
  • ระบบจะส่งพารามิเตอร์ style ไปยังวิธีการแสดงผลแถวที่ต้องแนบกับองค์ประกอบแถวด้วย รายการในลิสต์จะอยู่ในตำแหน่งแบบสัมบูรณ์โดยมีการกําหนดค่าความสูงและความกว้างในบรรทัด และพารามิเตอร์ style จะเป็นผู้รับผิดชอบในเรื่องนี้

ตัวอย่าง Glitch ที่แสดงไว้ก่อนหน้านี้ในบทความนี้คือตัวอย่างคอมโพเนนต์ FixedSizeList

กรณีที่ควรใช้รายการที่มีขนาดตัวแปร

ใช้คอมโพเนนต์ VariableSizeList เพื่อแสดงผลรายการที่มีขนาดต่างกัน คอมโพเนนต์นี้ทำงานในลักษณะเดียวกับรายการขนาดคงที่ แต่จะคาดหวังฟังก์ชันสำหรับพร็อพเพอร์ตี้ itemSize แทนค่าที่เจาะจง

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;

ชิ้นงานต่อไปนี้แสดงตัวอย่างของคอมโพเนนต์นี้

ฟังก์ชันขนาดสินค้าซึ่งส่งไปยังพร็อพ itemSize จะสุ่มความสูงของแถวในตัวอย่างนี้ อย่างไรก็ตาม ในแอปพลิเคชันจริงควรมีตรรกะจริงที่กําหนดขนาดของแต่ละรายการ ขนาดดังกล่าวควรคำนวณจากข้อมูลหรือได้มาจาก API

ตารางกริด

react-window ยังรองรับการจำลองรายการหรือตารางหลายมิติด้วย ในบริบทนี้ "กรอบเวลา" ของเนื้อหาที่มองเห็นได้จะเปลี่ยนแปลงเมื่อผู้ใช้เลื่อนในแนวนอนและแนวตั้ง

หน้าต่างเนื้อหาที่เลื่อนในตารางเสมือนเป็นแบบ 2 มิติ
การย้าย "กรอบ" ของเนื้อหาในตารางกริดเสมือนเป็นแบบ 2 มิติ

ในทำนองเดียวกัน คุณสามารถใช้คอมโพเนนต์ทั้ง FixedSizeGrid และ VariableSizeGrid ได้ ทั้งนี้ขึ้นอยู่กับว่าขนาดของรายการในรายการหนึ่งๆ จะแตกต่างกันหรือไม่

  • สำหรับ FixedSizeGrid API จะคล้ายกับของ แต่ต้องแสดงทั้งความสูง ความกว้าง และจํานวนรายการสําหรับทั้งคอลัมน์และแถว
  • สำหรับ VariableSizeGrid คุณสามารถเปลี่ยนทั้งความกว้างของคอลัมน์และความสูงของแถวได้ด้วยการส่งฟังก์ชันแทนค่าไปยังพร็อพที่เกี่ยวข้อง

ดูตัวอย่างกริดเสมือนได้จากเอกสารประกอบ

การโหลดแบบ Lazy Loading เมื่อเลื่อน

เว็บไซต์จำนวนมากเพิ่มประสิทธิภาพโดยการรอโหลดและแสดงผลรายการในรายการยาวๆ จนกว่าผู้ใช้จะเลื่อนลงได้ เทคนิคที่โดยทั่วไปเรียกว่า "การโหลดแบบไม่จำกัด" นี้จะเพิ่มโหนด DOM ใหม่ลงในรายการขณะที่ผู้ใช้เลื่อนผ่านเกณฑ์ที่กำหนดไว้ใกล้กับจุดท้ายสุด แม้ว่าวิธีนี้จะดีกว่าการโหลดรายการทั้งหมดในรายการพร้อมกัน แต่ท้ายที่สุดแล้ว DOM จะยังคงมีรายการแถวหลายพันรายการหากผู้ใช้เลื่อนผ่านรายการดังกล่าว ซึ่งอาจทําให้ DOM มีขนาดใหญ่เกินจําเป็น ซึ่งเริ่มส่งผลต่อประสิทธิภาพด้วยการทําให้การคํานวณสไตล์และการกลายพันธุ์ DOM ช้าลง

แผนภาพต่อไปนี้อาจช่วยสรุปข้อมูลนี้ได้

ความแตกต่างในการเลื่อนระหว่างรายการปกติกับรายการเสมือน
ความแตกต่างในการเลื่อนระหว่างรายการปกติและรายการเสมือนจริง

วิธีที่ดีที่สุดในการแก้ปัญหานี้คือใช้ไลบรารีอย่าง react-window ต่อไปเพื่อรักษา "กรอบเวลา" เล็กๆ ขององค์ประกอบในหน้าเว็บ แต่โหลดรายการใหม่แบบ Lazy Load ด้วยเมื่อผู้ใช้เลื่อนลง แพ็กเกจแยกต่างหากอย่าง react-window-infinite-loader จะช่วยให้คุณทำสิ่งเหล่านี้ได้โดยใช้ react-window

ลองดูโค้ดต่อไปนี้ซึ่งแสดงตัวอย่างสถานะที่จัดการในคอมโพเนนต์ App หลัก

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;

ระบบจะส่งเมธอด loadMore ไปยัง ListComponent ย่อยที่มีรายการตัวโหลดแบบไม่จำกัด สิ่งนี้มีความสำคัญเนื่องจากตัวโหลดอนันต์จำเป็นต้องเรียกใช้ Callback เพื่อโหลดรายการเพิ่มเติมเมื่อผู้ใช้เลื่อนผ่านจุดหนึ่งๆ

ListComponent ที่แสดงผลรายการอาจมีลักษณะดังนี้

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;

ในส่วนนี้ คอมโพเนนต์ FixedSizeList จะรวมอยู่ใน InfiniteLoader พร็อพที่กำหนดให้กับตัวโหลดมีดังนี้

  • isItemLoaded: วิธีการที่ตรวจสอบว่ารายการหนึ่งๆ โหลดแล้วหรือยัง
  • itemCount: จำนวนรายการในลิสต์ (หรือที่คาดไว้)
  • loadMoreItems: Callback ซึ่งให้คำมั่นสัญญาว่าจะแก้ไขปัญหาเพิ่มเติมสำหรับรายการ

พร็อพการเรนเดอร์ใช้เพื่อแสดงผลฟังก์ชันที่คอมโพเนนต์ลิสต์ใช้เพื่อแสดงผล ทั้งแอตทริบิวต์ onItemsRendered และ ref เป็นแอตทริบิวต์ที่ต้องส่งเข้ามา

ต่อไปนี้คือตัวอย่างวิธีที่การโหลดแบบไม่จำกัดทำงานร่วมกับรายการเสมือน

การเลื่อนดูรายการอาจดูเหมือนเดิม แต่ตอนนี้ระบบจะส่งคําขอดึงข้อมูลผู้ใช้ 10 รายจาก random user API ทุกครั้งที่คุณเลื่อนดูใกล้ถึงท้ายรายการ ซึ่งทั้งหมดนี้ทำได้ในขณะที่แสดงผลการค้นหา เพียง "หน้าต่าง" เดียวต่อครั้ง

เมื่อตรวจสอบ index ของรายการหนึ่งๆ ระบบจะแสดงสถานะการโหลดที่แตกต่างกันสำหรับรายการนั้นๆ โดยขึ้นอยู่กับว่ามีการส่งคำขอสำหรับรายการที่ใหม่กว่าและรายการดังกล่าวยังโหลดอยู่หรือไม่

เช่น

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

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

โอเวอร์สแกน

เนื่องจากรายการในรายการเสมือนจะเปลี่ยนแปลงเฉพาะเมื่อผู้ใช้เลื่อนดู พื้นที่ว่างจึงอาจกะพริบเป็นระยะเวลาสั้นๆ เมื่อรายการใหม่กำลังจะแสดง คุณสามารถลองเลื่อนดูตัวอย่างก่อนหน้านี้ในคู่มือนี้อย่างรวดเร็วเพื่อสังเกตการณ์นี้ได้

react-window จะให้คุณโอเวอร์สแกนรายการด้วยพร็อพเพอร์ตี้ overscanCount ได้ เพื่อปรับปรุงประสบการณ์ของผู้ใช้เกี่ยวกับรายการเสมือน ซึ่งช่วยให้คุณกำหนดจำนวนรายการที่อยู่นอก "กรอบ" ที่มองเห็นได้ให้แสดงผลได้ตลอดเวลา

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

overscanCount ใช้ได้กับทั้งคอมโพเนนต์ FixedSizeList และ VariableSizeList และมีค่าเริ่มต้นเป็น 1 การสแกนมากกว่า 1 รายการช่วยป้องกันพื้นที่ว่างที่กะพริบให้เห็นได้เมื่อผู้ใช้เลื่อน โดยขึ้นอยู่กับขนาดของรายการและขนาดของแต่ละรายการ อย่างไรก็ตาม การสแกนรายการมากเกินไปอาจส่งผลเสียต่อประสิทธิภาพ จุดสูงสุดในการใช้รายการเสมือนคือการลดจำนวนรายการที่ผู้ใช้จะเห็นในช่วงเวลาหนึ่งๆ ให้เหลือน้อยที่สุด ดังนั้นพยายามให้มีจำนวนรายการที่สแกนมากเกินไปให้น้อยที่สุดเท่าที่จะเป็นไปได้

สำหรับ FixedSizeGrid และ VariableSizeGrid ให้ใช้พร็อพเพอร์ตี้ overscanColumnsCount และ overscanRowsCount เพื่อควบคุมจำนวนคอลัมน์และแถวที่จะสแกนมาเกินขอบเขตตามลำดับ

บทสรุป

หากไม่แน่ใจว่าควรเริ่มการจำลองรายการและตารางในแอปพลิเคชันอย่างไร ให้ทำตามขั้นตอนต่อไปนี้

  1. วัดประสิทธิภาพการแสดงผลและการเลื่อน บทความนี้แสดงวิธีใช้เครื่องมือวัด FPS ในเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome เพื่อสำรวจประสิทธิภาพการแสดงผลรายการในรายการ
  2. ใส่ react-window สำหรับรายการหรือตารางกริดที่ยาวซึ่งส่งผลต่อประสิทธิภาพ
  3. หากมีฟีเจอร์บางอย่างที่ไม่รองรับใน react-window ให้พิจารณาใช้ react-virtualized หากคุณเพิ่มฟังก์ชันนี้ด้วยตนเองไม่ได้
  4. ตัดรายการเสมือนด้วย react-window-infinite-loader หากต้องการโหลดรายการแบบ Lazy Load เมื่อผู้ใช้เลื่อน
  5. ใช้พร็อพเพอร์ตี้ overscanCount สำหรับรายการ และพร็อพเพอร์ตี้ overscanColumnsCount และ overscanRowsCount สำหรับตารางกริดเพื่อป้องกันไม่ให้เนื้อหาว่างเปล่าปรากฏขึ้น อย่าสแกนรายการมากเกินไปเนื่องจากจะส่งผลเสียต่อประสิทธิภาพ