ไทย

คู่มือฉบับสมบูรณ์เกี่ยวกับกระบวนการ Reconciliation ของ React, การสำรวจอัลกอริทึม Virtual DOM diffing, เทคนิคการเพิ่มประสิทธิภาพ และผลกระทบต่อประสิทธิภาพการทำงาน

React Reconciliation: เปิดเผยเบื้องหลังอัลกอริทึม Virtual DOM Diffing

React ซึ่งเป็นไลบรารี JavaScript ยอดนิยมสำหรับสร้างส่วนติดต่อผู้ใช้ (user interfaces) มีประสิทธิภาพและความเร็วสูงได้ด้วยกระบวนการที่เรียกว่า reconciliation หัวใจสำคัญของ reconciliation คือ อัลกอริทึม Virtual DOM diffing ซึ่งเป็นกลไกที่ซับซ้อนที่ใช้ตัดสินใจว่าจะอัปเดต DOM (Document Object Model) จริงอย่างไรให้มีประสิทธิภาพสูงสุด บทความนี้จะเจาะลึกกระบวนการ reconciliation ของ React อธิบายเกี่ยวกับ Virtual DOM, อัลกอริทึม diffing และกลยุทธ์ที่ใช้ได้จริงเพื่อเพิ่มประสิทธิภาพการทำงาน

Virtual DOM คืออะไร?

Virtual DOM (VDOM) คือการจำลอง DOM จริงในรูปแบบที่เบาและทำงานในหน่วยความจำ (in-memory) ลองนึกภาพว่ามันเป็นพิมพ์เขียวของส่วนติดต่อผู้ใช้จริง แทนที่จะจัดการกับ DOM ของเบราว์เซอร์โดยตรง React จะทำงานกับการจำลองเสมือนนี้ เมื่อข้อมูลในคอมโพเนนต์ของ React เปลี่ยนแปลง จะมีการสร้าง Virtual DOM tree ขึ้นมาใหม่ จากนั้น tree ใหม่นี้จะถูกนำไปเปรียบเทียบกับ Virtual DOM tree ก่อนหน้า

ประโยชน์หลักของการใช้ Virtual DOM:

กระบวนการ Reconciliation: วิธีที่ React อัปเดต DOM

Reconciliation คือกระบวนการที่ React ใช้ซิงโครไนซ์ Virtual DOM กับ DOM จริง เมื่อ state ของคอมโพเนนต์เปลี่ยนแปลง React จะทำตามขั้นตอนต่อไปนี้:

  1. เรนเดอร์คอมโพเนนต์ใหม่: React จะเรนเดอร์คอมโพเนนต์ใหม่และสร้าง Virtual DOM tree ใหม่ขึ้นมา
  2. เปรียบเทียบ tree ใหม่และเก่า (Diffing): React จะเปรียบเทียบ Virtual DOM tree ใหม่กับ tree ก่อนหน้า นี่คือขั้นตอนที่อัลกอริทึม diffing เข้ามามีบทบาท
  3. ระบุชุดการเปลี่ยนแปลงที่น้อยที่สุด: อัลกอริทึม diffing จะระบุชุดการเปลี่ยนแปลงที่น้อยที่สุดที่จำเป็นต้องใช้เพื่ออัปเดต DOM จริง
  4. ปรับใช้การเปลี่ยนแปลง (Committing): React จะปรับใช้เฉพาะการเปลี่ยนแปลงเหล่านั้นกับ DOM จริง

อัลกอริทึม Diffing: ทำความเข้าใจกฎเกณฑ์

อัลกอริทึม diffing เป็นแกนหลักของกระบวนการ reconciliation ของ React มันใช้วิธีการแบบฮิวริสติก (heuristics) เพื่อหาวิธีที่มีประสิทธิภาพที่สุดในการอัปเดต DOM แม้ว่าจะไม่ได้รับประกันจำนวนการดำเนินการที่น้อยที่สุดในทุกกรณี แต่มันให้ประสิทธิภาพที่ยอดเยี่ยมในสถานการณ์ส่วนใหญ่ อัลกอริทึมนี้ทำงานภายใต้สมมติฐานต่อไปนี้:

คำอธิบายโดยละเอียดของอัลกอริทึม Diffing

เรามาดูรายละเอียดวิธีการทำงานของอัลกอริทึม diffing กัน:

  1. การเปรียบเทียบประเภทของอิลิเมนต์: ขั้นแรก React จะเปรียบเทียบ root elements ของ tree ทั้งสอง หากมีประเภทต่างกัน React จะรื้อ tree เก่าทิ้งและสร้าง tree ใหม่ขึ้นมาตั้งแต่ต้น ซึ่งรวมถึงการลบโหนด DOM เก่าและสร้างโหนด DOM ใหม่ด้วยประเภทอิลิเมนต์ใหม่
  2. การอัปเดตคุณสมบัติของ DOM: หากประเภทของอิลิเมนต์เหมือนกัน React จะเปรียบเทียบ attributes (props) ของอิลิเมนต์ทั้งสอง มันจะระบุว่า attributes ใดมีการเปลี่ยนแปลงและอัปเดตเฉพาะ attributes เหล่านั้นบนอิลิเมนต์ DOM จริง ตัวอย่างเช่น หาก className prop ของอิลิเมนต์ <div> เปลี่ยนแปลงไป React จะอัปเดต attribute className บนโหนด DOM ที่สอดคล้องกัน
  3. การอัปเดตคอมโพเนนต์: เมื่อ React พบอิลิเมนต์ที่เป็นคอมโพเนนต์ มันจะอัปเดตคอมโพเนนต์นั้นซ้ำๆ ซึ่งรวมถึงการเรนเดอร์คอมโพเนนต์ใหม่และใช้อัลกอริทึม diffing กับผลลัพธ์ของคอมโพเนนต์นั้น
  4. การ Diffing ลิสต์ (โดยใช้ Keys): การ diffing ลิสต์ของ children อย่างมีประสิทธิภาพเป็นสิ่งสำคัญต่อประสิทธิภาพการทำงาน เมื่อเรนเดอร์ลิสต์ React คาดหวังว่า child แต่ละตัวจะมี key prop ที่ไม่ซ้ำกัน key prop ช่วยให้ React สามารถระบุได้ว่ารายการใดถูกเพิ่ม, ลบ, หรือจัดลำดับใหม่

ตัวอย่าง: การ Diffing แบบมีและไม่มี Keys

แบบไม่มี Keys:

// การเรนเดอร์ครั้งแรก
<ul>
  <li>Item 1</li>
  <li>Item 2</li>
</ul>

// หลังจากเพิ่มรายการที่จุดเริ่มต้น
<ul>
  <li>Item 0</li>
  <li>Item 1</li>
  <li>Item 2</li>
</ul>

หากไม่มี key React จะสันนิษฐานว่าทั้งสามรายการมีการเปลี่ยนแปลง มันจะอัปเดตโหนด DOM สำหรับทุกรายการ แม้ว่าจะมีเพียงรายการใหม่ถูกเพิ่มเข้ามา ซึ่งไม่มีประสิทธิภาพ

แบบมี Keys:

// การเรนเดอร์ครั้งแรก
<ul>
  <li key="item1">Item 1</li>
  <li key="item2">Item 2</li>
</ul>

// หลังจากเพิ่มรายการที่จุดเริ่มต้น
<ul>
  <li key="item0">Item 0</li>
  <li key="item1">Item 1</li>
  <li key="item2">Item 2</li>
</ul>

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

เทคนิคการเพิ่มประสิทธิภาพ (Performance Optimization)

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

ตัวอย่างและการใช้งานจริง

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

ตัวอย่างที่ 1: ป้องกันการเรนเดอร์ใหม่ที่ไม่จำเป็นด้วย React.memo

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

import React from 'react';

const UserInfo = React.memo(function UserInfo(props) {
  console.log('Rendering UserInfo component');
  return (
    <div>
      <p>Name: {props.name}</p>
      <p>Age: {props.age}</p>
    </div>
  );
});

export default UserInfo;

React.memo จะทำการเปรียบเทียบ props ของคอมโพเนนต์แบบตื้นๆ (shallowly compares) หาก props เหมือนเดิม มันจะข้ามการเรนเดอร์ใหม่

ตัวอย่างที่ 2: การใช้โครงสร้างข้อมูลที่ไม่เปลี่ยนรูป (Immutable Data Structures)

พิจารณาคอมโพเนนต์ที่ได้รับลิสต์ของรายการเป็น prop หากลิสต์ถูกแก้ไขโดยตรง React อาจตรวจไม่พบการเปลี่ยนแปลงและอาจไม่เรนเดอร์คอมโพเนนต์ใหม่ การใช้โครงสร้างข้อมูลที่ไม่เปลี่ยนรูปสามารถป้องกันปัญหานี้ได้

import React from 'react';
import { List } from 'immutable';

function ItemList(props) {
  console.log('Rendering ItemList component');
  return (
    <ul>
      {props.items.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

export default ItemList;

ในตัวอย่างนี้ items prop ควรเป็น List ที่ไม่เปลี่ยนรูปจากไลบรารี Immutable.js เมื่อลิสต์มีการอัปเดต จะมีการสร้าง List ใหม่ที่ไม่เปลี่ยนรูปขึ้นมา ซึ่ง React สามารถตรวจจับได้ง่าย

ข้อผิดพลาดที่พบบ่อยและวิธีหลีกเลี่ยง

มีข้อผิดพลาดทั่วไปหลายอย่างที่อาจขัดขวางประสิทธิภาพของแอปพลิเคชัน React การทำความเข้าใจและหลีกเลี่ยงข้อผิดพลาดเหล่านี้เป็นสิ่งสำคัญ

ข้อควรพิจารณาสำหรับการพัฒนา React ในระดับสากล

เมื่อพัฒนาแอปพลิเคชัน React สำหรับผู้ใช้ทั่วโลก ควรพิจารณาสิ่งต่อไปนี้:

สรุป

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

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

React Reconciliation: ทำความเข้าใจอัลกอริทึม Virtual DOM Diffing | MLOG