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

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

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

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

มีประโยชน์อย่างไร

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

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

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

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

แสดงหน้าต่างรีแอ็กชัน

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 Loading ด้วยในขณะที่ผู้ใช้เลื่อนลง แพ็กเกจ 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: โค้ดเรียกกลับที่ส่งคืนคำมั่นสัญญาที่เปลี่ยนเป็นข้อมูลเพิ่มเติมของรายการ

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