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

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

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

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

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

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

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

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

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

react-window

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

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

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

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 Entry ใหม่กว่าเมื่อผู้ใช้เลื่อนลง แพ็กเกจแยกต่างหากอย่าง 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ลูกที่มี รายการตัวโหลดแบบไม่มีที่สิ้นสุด ค่านี้สำคัญเนื่องจากตัวโหลดแบบเลื่อนได้ไม่รู้จบต้องเรียกใช้ฟังก์ชันเรียกกลับเพื่อโหลดรายการเพิ่มเติมเมื่อผู้ใช้เลื่อนผ่านจุดหนึ่ง

โค้ด 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: การเรียกกลับที่แสดงผล Promise ซึ่งจะเปลี่ยนเป็นข้อมูลเพิ่มเติม สำหรับรายการ

Render Prop ใช้เพื่อแสดงผลฟังก์ชันที่คอมโพเนนต์รายการใช้ในการแสดงผล ทั้งแอตทริบิวต์ 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 สำหรับตาราง เพื่อป้องกันไม่ให้เนื้อหาว่างเปล่าปรากฏชั่วคราว อย่าสแกนรายการมากเกินไปเพราะ จะส่งผลเสียต่อประสิทธิภาพ