ไทย

คู่มือฉบับสมบูรณ์เกี่ยวกับฟีเจอร์ automatic batching ของ React พร้อมสำรวจประโยชน์ ข้อจำกัด และเทคนิคการปรับปรุงขั้นสูงเพื่อประสิทธิภาพแอปพลิเคชันที่ราบรื่นขึ้น

React Batching: การปรับปรุง State Updates เพื่อประสิทธิภาพสูงสุด

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

React Batching คืออะไร?

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

ก่อนหน้า React 18, batching จะเกิดขึ้นเฉพาะภายใน event handlers ของ React เท่านั้น การอัปเดต state ที่อยู่นอก handlers เหล่านี้ เช่น ภายใน setTimeout, promises หรือ native event handlers จะไม่ถูกทำ batching ซึ่งมักจะนำไปสู่การ re-render ที่ไม่คาดคิดและปัญหาคอขวดด้านประสิทธิภาพ

ด้วยการเปิดตัว automatic batching ใน React 18 ข้อจำกัดนี้ได้ถูกแก้ไขแล้ว ตอนนี้ React จะทำการ batch การอัปเดต state โดยอัตโนมัติในสถานการณ์ที่หลากหลายมากขึ้น รวมถึง:

ประโยชน์ของ React Batching

ประโยชน์ของ React batching นั้นมีความสำคัญและส่งผลโดยตรงต่อประสบการณ์ของผู้ใช้:

React Batching ทำงานอย่างไร

กลไก batching ของ React ถูกสร้างขึ้นในกระบวนการ reconciliation เมื่อมีการเรียกใช้การอัปเดต state, React จะไม่ re-render คอมโพเนนต์ทันที แต่จะเพิ่มการอัปเดตนั้นเข้าไปในคิว หากมีการอัปเดตหลายครั้งเกิดขึ้นในช่วงเวลาสั้นๆ React จะรวมการอัปเดตเหล่านั้นเป็นการอัปเดตเพียงครั้งเดียว จากนั้นการอัปเดตที่รวมกันนี้จะถูกใช้เพื่อ re-render คอมโพเนนต์เพียงครั้งเดียว ซึ่งสะท้อนการเปลี่ยนแปลงทั้งหมดในรอบเดียว

ลองพิจารณาตัวอย่างง่ายๆ ต่อไปนี้:


import React, { useState } from 'react';

function ExampleComponent() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  const handleClick = () => {
    setCount1(count1 + 1);
    setCount2(count2 + 1);
  };

  console.log('คอมโพเนนต์ถูก re-render');

  return (
    <div>
      <p>Count 1: {count1}</p>
      <p>Count 2: {count2}</p>
      <button onClick={handleClick}>Increment Both</button>
    </div>
  );
}

export default ExampleComponent;

ในตัวอย่างนี้ เมื่อคลิกปุ่ม ทั้ง setCount1 และ setCount2 จะถูกเรียกใช้ภายใน event handler เดียวกัน React จะทำการ batch การอัปเดต state ทั้งสองนี้และ re-render คอมโพเนนต์เพียงครั้งเดียวเท่านั้น คุณจะเห็นข้อความ "คอมโพเนนต์ถูก re-render" ถูกบันทึกลงในคอนโซลเพียงครั้งเดียวต่อการคลิก ซึ่งเป็นการแสดงให้เห็นการทำงานของ batching

การอัปเดตที่ไม่ถูก Batch: เมื่อ Batching ไม่ทำงาน

แม้ว่า React 18 จะมีการทำ automatic batching ในสถานการณ์ส่วนใหญ่ แต่ก็มีบางกรณีที่คุณอาจต้องการข้าม batching และบังคับให้ React อัปเดตคอมโพเนนต์ทันที ซึ่งโดยทั่วไปจำเป็นเมื่อคุณต้องการอ่านค่า DOM ที่อัปเดตแล้วทันทีหลังจากการอัปเดต state

React มี flushSync API เพื่อวัตถุประสงค์นี้ flushSync จะบังคับให้ React ทำการ flush การอัปเดตที่ค้างอยู่ทั้งหมดแบบซิงโครนัสและอัปเดต DOM ทันที

นี่คือตัวอย่าง:


import React, { useState } from 'react';
import { flushSync } from 'react-dom';

function ExampleComponent() {
  const [text, setText] = useState('');

  const handleChange = (event) => {
    flushSync(() => {
      setText(event.target.value);
    });
    console.log('ค่าของ input หลังอัปเดต:', event.target.value);
  };

  return (
    <input type="text" value={text} onChange={handleChange} />
  );
}

export default ExampleComponent;

ในตัวอย่างนี้ flushSync ถูกใช้เพื่อให้แน่ใจว่า state ของ text ถูกอัปเดตทันทีหลังจากค่าของ input เปลี่ยนแปลง ซึ่งช่วยให้คุณสามารถอ่านค่าที่อัปเดตแล้วในฟังก์ชัน handleChange ได้โดยไม่ต้องรอรอบการ render ถัดไป อย่างไรก็ตาม ควรใช้ flushSync อย่างระมัดระวังเนื่องจากอาจส่งผลเสียต่อประสิทธิภาพได้

เทคนิคการปรับปรุงประสิทธิภาพขั้นสูง

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

1. การใช้ Functional Updates

เมื่ออัปเดต state โดยอิงจากค่าก่อนหน้า แนวทางปฏิบัติที่ดีที่สุดคือการใช้ functional updates ซึ่งจะช่วยให้มั่นใจได้ว่าคุณกำลังทำงานกับค่า state ที่เป็นปัจจุบันที่สุด โดยเฉพาะในสถานการณ์ที่เกี่ยวข้องกับการทำงานแบบอะซิงโครนัสหรือการอัปเดตแบบ batch

แทนที่จะใช้:


setCount(count + 1);

ให้ใช้:


setCount((prevCount) => prevCount + 1);

Functional updates ช่วยป้องกันปัญหาที่เกี่ยวข้องกับ stale closures และรับประกันการอัปเดต state ที่ถูกต้อง

2. การไม่เปลี่ยนแปลงข้อมูล (Immutability)

การจัดการ state แบบไม่เปลี่ยนแปลงข้อมูล (immutable) เป็นสิ่งสำคัญสำหรับการเรนเดอร์ที่มีประสิทธิภาพใน React เมื่อ state เป็นแบบ immutable, React สามารถตัดสินใจได้อย่างรวดเร็วว่าคอมโพเนนต์จำเป็นต้อง re-render หรือไม่ โดยการเปรียบเทียบ reference ของค่า state เก่าและใหม่ หาก reference แตกต่างกัน React จะรู้ว่า state มีการเปลี่ยนแปลงและจำเป็นต้อง re-render แต่ถ้า reference เหมือนกัน React สามารถข้ามการ re-render ไปได้ ซึ่งช่วยประหยัดเวลาในการประมวลผลอันมีค่า

เมื่อทำงานกับอ็อบเจ็กต์หรืออาร์เรย์ ควรหลีกเลี่ยงการแก้ไข state ที่มีอยู่โดยตรง แต่ให้สร้างสำเนาใหม่ของอ็อบเจ็กต์หรืออาร์เรย์พร้อมกับการเปลี่ยนแปลงที่ต้องการ

ตัวอย่างเช่น แทนที่จะใช้:


const updatedItems = items;
updatedItems.push(newItem);
setItems(updatedItems);

ให้ใช้:


setItems([...items, newItem]);

Spread operator (...) จะสร้างอาร์เรย์ใหม่พร้อมกับรายการที่มีอยู่และรายการใหม่ที่ต่อท้าย

3. การทำ Memoization

Memoization เป็นเทคนิคการปรับปรุงประสิทธิภาพที่ทรงพลังซึ่งเกี่ยวข้องกับการแคชผลลัพธ์ของการเรียกใช้ฟังก์ชันที่มีค่าใช้จ่ายสูงและส่งคืนผลลัพธ์ที่แคชไว้เมื่อมีการเรียกใช้ด้วยอินพุตเดิมอีกครั้ง React มีเครื่องมือ memoization หลายอย่าง รวมถึง React.memo, useMemo, และ useCallback

นี่คือตัวอย่างการใช้ React.memo:


import React from 'react';

const MyComponent = React.memo(({ data }) => {
  console.log('MyComponent re-rendered');
  return <div>{data.name}</div>;
});

export default MyComponent;

ในตัวอย่างนี้ MyComponent จะ re-render ก็ต่อเมื่อ prop data มีการเปลี่ยนแปลงเท่านั้น

4. การทำ Code Splitting

Code splitting คือการแบ่งแอปพลิเคชันของคุณออกเป็นส่วนเล็กๆ ที่สามารถโหลดได้ตามความต้องการ ซึ่งจะช่วยลดเวลาในการโหลดเริ่มต้นและปรับปรุงประสิทธิภาพโดยรวมของแอปพลิเคชันของคุณ React มีหลายวิธีในการทำ code splitting รวมถึง dynamic imports และคอมโพเนนต์ React.lazy และ Suspense

นี่คือตัวอย่างการใช้ React.lazy และ Suspense:


import React, { Suspense } from 'react';

const MyComponent = React.lazy(() => import('./MyComponent'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <MyComponent />
    </Suspense>
  );
}

export default App;

ในตัวอย่างนี้ MyComponent จะถูกโหลดแบบอะซิงโครนัสโดยใช้ React.lazy ส่วนคอมโพเนนต์ Suspense จะแสดง UI สำรองในขณะที่คอมโพเนนต์กำลังโหลด

5. การทำ Virtualization

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

ไลบรารีอย่าง react-virtualized และ react-window มีคอมโพเนนต์สำหรับนำ virtualization ไปใช้ในแอปพลิเคชัน React

6. การทำ Debouncing และ Throttling

Debouncing และ throttling เป็นเทคนิคในการจำกัดอัตราการทำงานของฟังก์ชัน Debouncing จะหน่วงเวลาการทำงานของฟังก์ชันจนกว่าจะไม่มีการใช้งานเป็นระยะเวลาหนึ่ง ส่วน Throttling จะทำงานฟังก์ชันไม่เกินหนึ่งครั้งภายในช่วงเวลาที่กำหนด

เทคนิคเหล่านี้มีประโยชน์อย่างยิ่งในการจัดการกับ event ที่เกิดขึ้นอย่างรวดเร็ว เช่น scroll events, resize events และ input events การทำ debouncing หรือ throttling ให้กับ event เหล่านี้จะช่วยป้องกันการ re-render ที่มากเกินไปและปรับปรุงประสิทธิภาพ

ตัวอย่างเช่น คุณสามารถใช้ฟังก์ชัน lodash.debounce เพื่อทำ debounce ให้กับ input event:


import React, { useState, useCallback } from 'react';
import debounce from 'lodash.debounce';

function ExampleComponent() {
  const [text, setText] = useState('');

  const handleChange = useCallback(
    debounce((event) => {
      setText(event.target.value);
    }, 300),
    []
  );

  return (
    <input type="text" onChange={handleChange} />
  );
}

export default ExampleComponent;

ในตัวอย่างนี้ ฟังก์ชัน handleChange ถูกทำ debounce ด้วยการหน่วงเวลา 300 มิลลิวินาที ซึ่งหมายความว่าฟังก์ชัน setText จะถูกเรียกใช้หลังจากผู้ใช้หยุดพิมพ์เป็นเวลา 300 มิลลิวินาทีแล้วเท่านั้น

ตัวอย่างการใช้งานจริงและกรณีศึกษา

เพื่อแสดงให้เห็นถึงผลกระทบในทางปฏิบัติของ React batching และเทคนิคการปรับปรุงประสิทธิภาพ ลองพิจารณาตัวอย่างการใช้งานจริงบางส่วน:

การดีบักปัญหาเกี่ยวกับ Batching

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

แนวทางปฏิบัติที่ดีที่สุดสำหรับการปรับปรุง State Updates

โดยสรุป นี่คือแนวทางปฏิบัติที่ดีที่สุดสำหรับการปรับปรุงการอัปเดต state ใน React:

บทสรุป

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

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