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

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

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 จะเป็นผู้รับผิดชอบในส่วนนี้

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

ใช้คอมโพเนนต์ 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 ต่อไปเพื่อรักษา "หน้าต่าง" องค์ประกอบขนาดเล็กในหน้าเว็บ แต่ยังโหลดรายการใหม่เมื่อจำเป็นเมื่อผู้ใช้เลื่อนลงด้วย A separate package, react-window-infinite-loader, makes this possible with 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 ซึ่งเป็นคอมโพเนนต์ย่อยที่มีรายการตัวโหลดแบบไม่สิ้นสุด ซึ่งมีความสำคัญเนื่องจากตัวโหลดแบบไม่สิ้นสุดต้องทริกเกอร์การเรียกกลับเพื่อโหลดรายการเพิ่มเติมเมื่อผู้ใช้เลื่อนผ่านจุดที่กำหนด

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 ที่แสดงผล Promise ที่แสดงข้อมูลเพิ่มเติมสำหรับรายการ

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

การเลื่อนลงในรายการอาจให้ความรู้สึกเหมือนเดิม แต่ตอนนี้ระบบจะส่งคำขอเพื่อ ดึงผู้ใช้ 10 รายจาก 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 หากคุณต้องการโหลดรายการเมื่อจำเป็นเมื่อผู้ใช้เลื่อน
  5. ใช้พร็อพเพอร์ตี้ overscanCount สำหรับรายการ และพร็อพเพอร์ตี้ overscanColumnsCount และ overscanRowsCount สำหรับตารางกริดเพื่อป้องกันการกะพริบของเนื้อหาว่าง อย่าสแกนรายการมากเกินไปเนื่องจากจะส่งผลเสียต่อประสิทธิภาพ