สำรวจเทคนิค React memoization ขั้นสูงเพื่อเพิ่มประสิทธิภาพในแอปพลิเคชันระดับโลก เรียนรู้ว่าเมื่อใดและอย่างไรที่ควรใช้ React.memo, useCallback, useMemo และอื่น ๆ เพื่อสร้าง UI ที่มีประสิทธิภาพ
React Memo: เจาะลึกเทคนิคการปรับปรุงประสิทธิภาพสำหรับแอปพลิเคชันระดับโลก
React เป็นไลบรารี JavaScript ที่ทรงพลังสำหรับการสร้างส่วนติดต่อผู้ใช้ (user interfaces) แต่เมื่อแอปพลิเคชันมีความซับซ้อนมากขึ้น การปรับปรุงประสิทธิภาพ (performance optimization) ก็กลายเป็นสิ่งสำคัญ เครื่องมือที่จำเป็นอย่างหนึ่งในชุดเครื่องมือการปรับปรุงประสิทธิภาพของ React คือ React.memo
บล็อกโพสต์นี้จะให้คำแนะนำที่ครอบคลุมเพื่อทำความเข้าใจและใช้งาน React.memo
และเทคนิคที่เกี่ยวข้องอย่างมีประสิทธิภาพ เพื่อสร้างแอปพลิเคชัน React ที่มีประสิทธิภาพสูงสำหรับผู้ใช้ทั่วโลก
React.memo คืออะไร?
React.memo
คือ higher-order component (HOC) ที่ทำการ memoize ให้กับ functional component พูดง่ายๆ ก็คือ มันจะป้องกันไม่ให้ component ทำการ re-render หาก props ของมันไม่มีการเปลี่ยนแปลง โดยค่าเริ่มต้น มันจะทำการเปรียบเทียบ props แบบตื้น (shallow comparison) ซึ่งสามารถปรับปรุงประสิทธิภาพได้อย่างมาก โดยเฉพาะสำหรับ component ที่มีการเรนเดอร์ที่ใช้การคำนวณสูง หรือ re-render บ่อยครั้งแม้ว่า props จะยังคงเหมือนเดิมก็ตาม
ลองนึกภาพ component ที่แสดงโปรไฟล์ของผู้ใช้ หากข้อมูลของผู้ใช้ (เช่น ชื่อ, รูปโปรไฟล์) ไม่มีการเปลี่ยนแปลง ก็ไม่จำเป็นต้อง re-render component นั้นใหม่ React.memo
ช่วยให้คุณสามารถข้ามการ re-render ที่ไม่จำเป็นนี้ไปได้ ซึ่งช่วยประหยัดเวลาในการประมวลผลอันมีค่า
ทำไมต้องใช้ React.memo?
นี่คือประโยชน์หลักของการใช้ React.memo
:
- การปรับปรุงประสิทธิภาพ: ป้องกันการ re-render ที่ไม่จำเป็น ส่งผลให้ส่วนติดต่อผู้ใช้เร็วและราบรื่นขึ้น
- ลดการใช้งาน CPU: การ re-render ที่น้อยลงหมายถึงการใช้งาน CPU ที่น้อยลง ซึ่งมีความสำคัญอย่างยิ่งสำหรับอุปกรณ์พกพาและผู้ใช้ในพื้นที่ที่มีแบนด์วิดท์จำกัด
- ประสบการณ์ผู้ใช้ที่ดีขึ้น: แอปพลิเคชันที่ตอบสนองได้ดีขึ้นจะมอบประสบการณ์ผู้ใช้ที่ดีกว่า โดยเฉพาะสำหรับผู้ใช้ที่มีการเชื่อมต่ออินเทอร์เน็ตที่ช้าหรือใช้อุปกรณ์รุ่นเก่า
การใช้งาน React.memo ขั้นพื้นฐาน
การใช้ React.memo
นั้นตรงไปตรงมา เพียงแค่ห่อ functional component ของคุณด้วยมัน:
import React from 'react';
const MyComponent = (props) => {
console.log('MyComponent rendered');
return (
{props.data}
);
};
export default React.memo(MyComponent);
ในตัวอย่างนี้ MyComponent
จะ re-render ก็ต่อเมื่อ prop ที่ชื่อ data
เปลี่ยนแปลงเท่านั้น คำสั่ง console.log
จะช่วยให้คุณตรวจสอบได้ว่า component re-render จริงเมื่อใด
การทำความเข้าใจ Shallow Comparison
โดยค่าเริ่มต้น React.memo
จะทำการเปรียบเทียบ props แบบตื้น (shallow comparison) ซึ่งหมายความว่ามันจะตรวจสอบว่า reference ของ props เปลี่ยนแปลงไปหรือไม่ ไม่ใช่ค่าที่อยู่ภายใน สิ่งนี้สำคัญที่ต้องทำความเข้าใจเมื่อต้องจัดการกับ object และ array
พิจารณาตัวอย่างต่อไปนี้:
import React, { useState } from 'react';
const MyComponent = (props) => {
console.log('MyComponent rendered');
return (
{props.data.name}
);
};
const MemoizedComponent = React.memo(MyComponent);
const App = () => {
const [user, setUser] = useState({ name: 'John', age: 30 });
const handleClick = () => {
setUser({ ...user }); // สร้าง object ใหม่ที่มีค่าเหมือนเดิม
};
return (
);
};
export default App;
ในกรณีนี้ ถึงแม้ว่าค่าใน object user
(name
และ age
) จะยังคงเหมือนเดิม แต่ฟังก์ชัน handleClick
จะสร้าง object reference ใหม่ทุกครั้งที่ถูกเรียก ดังนั้น React.memo
จะเห็นว่า prop data
มีการเปลี่ยนแปลง (เพราะ object reference แตกต่างกัน) และจะทำการ re-render MyComponent
ฟังก์ชันเปรียบเทียบแบบกำหนดเอง (Custom Comparison Function)
เพื่อจัดการกับปัญหาของการเปรียบเทียบแบบตื้นกับ object และ array React.memo
อนุญาตให้คุณส่งฟังก์ชันเปรียบเทียบแบบกำหนดเองเป็นอาร์กิวเมนต์ที่สองได้ ฟังก์ชันนี้รับอาร์กิวเมนต์สองตัวคือ prevProps
และ nextProps
มันควรจะคืนค่า true
หาก component *ไม่ควร* re-render (กล่าวคือ props มีค่าเทียบเท่ากัน) และคืนค่า false
หากควร re-render
นี่คือวิธีที่คุณสามารถใช้ฟังก์ชันเปรียบเทียบแบบกำหนดเองในตัวอย่างก่อนหน้านี้:
import React, { useState, memo } from 'react';
const MyComponent = (props) => {
console.log('MyComponent rendered');
return (
{props.data.name}
);
};
const areEqual = (prevProps, nextProps) => {
return prevProps.data.name === nextProps.data.name && prevProps.data.age === nextProps.data.age;
};
const MemoizedComponent = memo(MyComponent, areEqual);
const App = () => {
const [user, setUser] = useState({ name: 'John', age: 30 });
const handleClick = () => {
setUser({ ...user });
};
return (
);
};
export default App;
ในตัวอย่างที่อัปเดตนี้ ฟังก์ชัน areEqual
จะเปรียบเทียบ property name
และ age
ของ object user
ตอนนี้ MemoizedComponent
จะ re-render ก็ต่อเมื่อ name
หรือ age
เปลี่ยนแปลงเท่านั้น
เมื่อใดที่ควรใช้ React.memo
React.memo
มีประสิทธิภาพสูงสุดในสถานการณ์ต่อไปนี้:
- Component ที่ได้รับ props เดิมบ่อยครั้ง: หาก props ของ component แทบจะไม่เปลี่ยนแปลง การใช้
React.memo
สามารถป้องกันการ re-render ที่ไม่จำเป็นได้ - Component ที่มีการเรนเดอร์ที่ใช้การคำนวณสูง: สำหรับ component ที่ทำการคำนวณที่ซับซ้อนหรือเรนเดอร์ข้อมูลจำนวนมาก การข้ามการ re-render สามารถปรับปรุงประสิทธิภาพได้อย่างมาก
- Pure functional components: Component ที่ให้ผลลัพธ์เหมือนเดิมสำหรับอินพุตเดิม เป็นตัวเลือกที่เหมาะสำหรับ
React.memo
อย่างไรก็ตาม สิ่งสำคัญที่ต้องทราบคือ React.memo
ไม่ใช่ยาวิเศษ การใช้งานอย่างไม่บันยะบันยังอาจส่งผลเสียต่อประสิทธิภาพได้ เพราะการเปรียบเทียบแบบตื้นเองก็มีค่าใช้จ่าย ดังนั้นจึงเป็นเรื่องสำคัญที่จะต้องทำการ profile แอปพลิเคชันของคุณและระบุ component ที่จะได้รับประโยชน์สูงสุดจากการทำ memoization
ทางเลือกอื่นนอกเหนือจาก React.memo
แม้ว่า React.memo
จะเป็นเครื่องมือที่ทรงพลัง แต่ก็ไม่ใช่ทางเลือกเดียวสำหรับการปรับปรุงประสิทธิภาพของ component ใน React นี่คือทางเลือกและเทคนิคเสริมบางอย่าง:
1. PureComponent
สำหรับ class components PureComponent
ให้ฟังก์ชันการทำงานที่คล้ายกับ React.memo
โดยมันจะทำการเปรียบเทียบแบบตื้นทั้ง props และ state และจะ re-render ก็ต่อเมื่อมีการเปลี่ยนแปลงเท่านั้น
import React from 'react';
class MyComponent extends React.PureComponent {
render() {
console.log('MyComponent rendered');
return (
{this.props.data}
);
}
}
export default MyComponent;
PureComponent
เป็นทางเลือกที่สะดวกสบายแทนการเขียน shouldComponentUpdate
ด้วยตนเอง ซึ่งเป็นวิธีดั้งเดิมในการป้องกันการ re-render ที่ไม่จำเป็นใน class components
2. shouldComponentUpdate
shouldComponentUpdate
เป็น lifecycle method ใน class components ที่ช่วยให้คุณสามารถกำหนดตรรกะเองได้ว่าจะให้ component re-render หรือไม่ มันให้ความยืดหยุ่นสูงสุด แต่ก็ต้องใช้ความพยายามด้วยตนเองมากขึ้น
import React from 'react';
class MyComponent extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
return nextProps.data !== this.props.data;
}
render() {
console.log('MyComponent rendered');
return (
{this.props.data}
);
}
}
export default MyComponent;
แม้ว่า shouldComponentUpdate
จะยังคงใช้งานได้ แต่โดยทั่วไปแล้ว PureComponent
และ React.memo
มักเป็นที่นิยมมากกว่าเนื่องจากความเรียบง่ายและใช้งานง่าย
3. useCallback
useCallback
คือ React hook ที่ทำการ memoize ให้กับฟังก์ชัน มันจะคืนค่าฟังก์ชันเวอร์ชันที่ถูก memoize ซึ่งจะเปลี่ยนแปลงก็ต่อเมื่อ dependency ตัวใดตัวหนึ่งของมันเปลี่ยนแปลงไปเท่านั้น สิ่งนี้มีประโยชน์อย่างยิ่งสำหรับการส่ง callbacks เป็น props ไปยัง memoized components
พิจารณาตัวอย่างต่อไปนี้:
import React, { useState, useCallback, memo } from 'react';
const MyComponent = (props) => {
console.log('MyComponent rendered');
return (
);
};
const MemoizedComponent = memo(MyComponent);
const App = () => {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount(count + 1);
}, [count]);
return (
Count: {count}
);
};
export default App;
ในตัวอย่างนี้ useCallback
ช่วยให้มั่นใจได้ว่าฟังก์ชัน handleClick
จะเปลี่ยนแปลงก็ต่อเมื่อ state count
เปลี่ยนแปลงเท่านั้น หากไม่มี useCallback
ฟังก์ชันใหม่จะถูกสร้างขึ้นทุกครั้งที่ App
re-render ซึ่งทำให้ MemoizedComponent
re-render โดยไม่จำเป็น
4. useMemo
useMemo
คือ React hook ที่ทำการ memoize ค่า มันจะคืนค่าที่ถูก memoize ซึ่งจะเปลี่ยนแปลงก็ต่อเมื่อ dependency ตัวใดตัวหนึ่งของมันเปลี่ยนแปลงไปเท่านั้น สิ่งนี้มีประโยชน์สำหรับการหลีกเลี่ยงการคำนวณที่มีค่าใช้จ่ายสูงซึ่งไม่จำเป็นต้องทำงานใหม่ทุกครั้งที่ re-render
import React, { useState, useMemo } from 'react';
const App = () => {
const [input, setInput] = useState('');
const expensiveCalculation = (str) => {
console.log('Calculating...');
let result = 0;
for (let i = 0; i < str.length * 1000000; i++) {
result++;
}
return result;
};
const memoizedResult = useMemo(() => expensiveCalculation(input), [input]);
return (
setInput(e.target.value)} />
Result: {memoizedResult}
);
};
export default App;
ในตัวอย่างนี้ useMemo
ช่วยให้มั่นใจได้ว่าฟังก์ชัน expensiveCalculation
จะถูกเรียกใช้ก็ต่อเมื่อ state input
เปลี่ยนแปลงเท่านั้น สิ่งนี้ช่วยป้องกันไม่ให้การคำนวณทำงานซ้ำทุกครั้งที่ re-render ซึ่งสามารถปรับปรุงประสิทธิภาพได้อย่างมาก
ตัวอย่างการใช้งานจริงสำหรับแอปพลิเคชันระดับโลก
ลองพิจารณาตัวอย่างการใช้งานจริงบางอย่างว่า React.memo
และเทคนิคที่เกี่ยวข้องสามารถนำไปใช้ในแอปพลิเคชันระดับโลกได้อย่างไร:
1. ตัวเลือกภาษา (Language Selector)
component ตัวเลือกภาษามักจะเรนเดอร์รายการภาษาที่มีอยู่ รายการนี้อาจค่อนข้างคงที่ หมายความว่าไม่เปลี่ยนแปลงบ่อยนัก การใช้ React.memo
สามารถป้องกันไม่ให้ตัวเลือกภาษา re-render โดยไม่จำเป็นเมื่อส่วนอื่น ๆ ของแอปพลิเคชันอัปเดต
import React, { memo } from 'react';
const LanguageItem = ({ language, onSelect }) => {
console.log(`LanguageItem ${language} rendered`);
return (
onSelect(language)}>{language}
);
};
const MemoizedLanguageItem = memo(LanguageItem);
const LanguageSelector = ({ languages, onSelect }) => {
return (
{languages.map((language) => (
))}
);
};
export default LanguageSelector;
ในตัวอย่างนี้ MemoizedLanguageItem
จะ re-render ก็ต่อเมื่อ prop language
หรือ onSelect
เปลี่ยนแปลงเท่านั้น ซึ่งจะเป็นประโยชน์อย่างยิ่งหากรายการภาษามีความยาวหรือถ้า handler onSelect
มีความซับซ้อน
2. ตัวแปลงสกุลเงิน (Currency Converter)
component ตัวแปลงสกุลเงินอาจแสดงรายการสกุลเงินและอัตราแลกเปลี่ยน อัตราแลกเปลี่ยนอาจมีการอัปเดตเป็นระยะ แต่รายการสกุลเงินอาจค่อนข้างคงที่ การใช้ React.memo
สามารถป้องกันไม่ให้รายการสกุลเงิน re-render โดยไม่จำเป็นเมื่ออัตราแลกเปลี่ยนอัปเดต
import React, { memo } from 'react';
const CurrencyItem = ({ currency, rate, onSelect }) => {
console.log(`CurrencyItem ${currency} rendered`);
return (
onSelect(currency)}>{currency} - {rate}
);
};
const MemoizedCurrencyItem = memo(CurrencyItem);
const CurrencyConverter = ({ currencies, onSelect }) => {
return (
{Object.entries(currencies).map(([currency, rate]) => (
))}
);
};
export default CurrencyConverter;
ในตัวอย่างนี้ MemoizedCurrencyItem
จะ re-render ก็ต่อเมื่อ prop currency
, rate
หรือ onSelect
เปลี่ยนแปลงเท่านั้น สิ่งนี้สามารถปรับปรุงประสิทธิภาพได้หากรายการสกุลเงินมีความยาวหรือถ้าการอัปเดตอัตราแลกเปลี่ยนเกิดขึ้นบ่อยครั้ง
3. การแสดงโปรไฟล์ผู้ใช้ (User Profile Display)
การแสดงโปรไฟล์ผู้ใช้เกี่ยวข้องกับการแสดงข้อมูลคงที่ เช่น ชื่อ รูปโปรไฟล์ และอาจรวมถึงประวัติส่วนตัว การใช้ `React.memo` ช่วยให้มั่นใจได้ว่า component จะ re-render ก็ต่อเมื่อข้อมูลผู้ใช้เปลี่ยนแปลงจริง ๆ ไม่ใช่ทุกครั้งที่ parent component อัปเดต
import React, { memo } from 'react';
const UserProfile = ({ user }) => {
console.log('UserProfile rendered');
return (
{user.name}
{user.bio}
);
};
export default memo(UserProfile);
สิ่งนี้มีประโยชน์อย่างยิ่งหาก UserProfile
เป็นส่วนหนึ่งของแดชบอร์ดหรือแอปพลิเคชันขนาดใหญ่ที่มีการอัปเดตบ่อยครั้ง แต่ข้อมูลผู้ใช้เองไม่ได้เปลี่ยนแปลงบ่อยนัก
ข้อผิดพลาดที่พบบ่อยและวิธีหลีกเลี่ยง
แม้ว่า React.memo
จะเป็นเครื่องมือเพิ่มประสิทธิภาพที่มีคุณค่า แต่สิ่งสำคัญคือต้องระวังข้อผิดพลาดที่พบบ่อยและวิธีหลีกเลี่ยง:
- Over-memoization: การใช้
React.memo
อย่างไม่บันยะบันยังอาจส่งผลเสียต่อประสิทธิภาพได้ เพราะการเปรียบเทียบแบบตื้นเองก็มีค่าใช้จ่าย ควร memoize เฉพาะ component ที่มีแนวโน้มจะได้รับประโยชน์จากมันเท่านั้น - Dependency arrays ที่ไม่ถูกต้อง: เมื่อใช้
useCallback
และuseMemo
ตรวจสอบให้แน่ใจว่าคุณระบุ dependency array ที่ถูกต้อง การละเลย dependencies หรือการใส่ dependencies ที่ไม่จำเป็นอาจนำไปสู่พฤติกรรมที่ไม่คาดคิดและปัญหาด้านประสิทธิภาพ - การแก้ไข props โดยตรง (Mutating props): หลีกเลี่ยงการแก้ไข props โดยตรง เพราะจะทำให้การเปรียบเทียบแบบตื้นของ
React.memo
ถูกข้ามไป ควรสร้าง object หรือ array ใหม่เสมอเมื่ออัปเดต props - ตรรกะการเปรียบเทียบที่ซับซ้อน: หลีกเลี่ยงตรรกะการเปรียบเทียบที่ซับซ้อนในฟังก์ชันเปรียบเทียบแบบกำหนดเอง เนื่องจากอาจลบล้างประโยชน์ด้านประสิทธิภาพของ
React.memo
ได้ พยายามทำให้ตรรกะการเปรียบเทียบเรียบง่ายและมีประสิทธิภาพมากที่สุด
การทำโปรไฟล์แอปพลิเคชันของคุณ (Profiling Your Application)
วิธีที่ดีที่สุดในการพิจารณาว่า React.memo
ช่วยปรับปรุงประสิทธิภาพได้จริงหรือไม่ คือการทำโปรไฟล์แอปพลิเคชันของคุณ React มีเครื่องมือหลายอย่างสำหรับการทำโปรไฟล์ รวมถึง React DevTools Profiler และ React.Profiler
API
React DevTools Profiler ช่วยให้คุณสามารถบันทึกร่องรอยประสิทธิภาพ (performance traces) ของแอปพลิเคชันและระบุ component ที่ re-render บ่อยครั้ง ส่วน React.Profiler
API ช่วยให้คุณสามารถวัดเวลาในการเรนเดอร์ของ component ที่ต้องการได้โดยตรงผ่านโค้ด
ด้วยการทำโปรไฟล์แอปพลิเคชันของคุณ คุณจะสามารถระบุ component ที่จะได้รับประโยชน์สูงสุดจากการทำ memoization และตรวจสอบให้แน่ใจว่า React.memo
กำลังช่วยปรับปรุงประสิทธิภาพได้จริง
บทสรุป
React.memo
เป็นเครื่องมือที่ทรงพลังสำหรับการปรับปรุงประสิทธิภาพของ component ใน React ด้วยการป้องกันการ re-render ที่ไม่จำเป็น มันสามารถปรับปรุงความเร็วและการตอบสนองของแอปพลิเคชันของคุณ ซึ่งนำไปสู่ประสบการณ์ผู้ใช้ที่ดีขึ้น อย่างไรก็ตาม สิ่งสำคัญคือต้องใช้ React.memo
อย่างรอบคอบและทำโปรไฟล์แอปพลิเคชันของคุณเพื่อให้แน่ใจว่ามันกำลังช่วยปรับปรุงประสิทธิภาพได้จริง
ด้วยการทำความเข้าใจแนวคิดและเทคนิคที่กล่าวถึงในบล็อกโพสต์นี้ คุณสามารถใช้ React.memo
และเทคนิคที่เกี่ยวข้องได้อย่างมีประสิทธิภาพเพื่อสร้างแอปพลิเคชัน React ที่มีประสิทธิภาพสูงสำหรับผู้ใช้ทั่วโลก เพื่อให้แน่ใจว่าแอปพลิเคชันของคุณรวดเร็วและตอบสนองได้ดีสำหรับผู้ใช้ทั่วโลก
อย่าลืมพิจารณาปัจจัยระดับโลก เช่น ความหน่วงของเครือข่าย (network latency) และความสามารถของอุปกรณ์เมื่อทำการปรับปรุงประสิทธิภาพแอปพลิเคชัน React ของคุณ ด้วยการมุ่งเน้นไปที่ประสิทธิภาพและการเข้าถึงได้ คุณสามารถสร้างแอปพลิเคชันที่มอบประสบการณ์ที่ยอดเยี่ยมสำหรับผู้ใช้ทุกคน ไม่ว่าพวกเขาจะอยู่ที่ไหนหรือใช้อุปกรณ์ใดก็ตาม