Mục lục

React Rendering & Virtual DOM Deep Dive

Hiểu sâu về cơ chế Virtual DOM, Reconciliation Algorithm và cách React tối ưu hiệu năng rendering

Tổng quan

Để tối ưu được ứng dụng React, bạn cần hiểu rõ cơ chế hoạt động bên trong của React. Bài viết này sẽ giải thích chi tiết về Virtual DOM, Reconciliation Algorithm và cách React quyết định khi nào cần re-render.


1. Virtual DOM là gì?

React Virtual DOM Reconciliation

Giải thích chi tiết hình minh họa:

Cột 1 - Real DOM (DOM thật trong Browser):

  • DOM thật là cấu trúc cây HTML thực tế mà browser hiển thị
  • Mỗi lần thay đổi DOM, browser phải re-render (tính toán lại layout, paint, composite)
  • Thao tác trực tiếp với DOM rất chậm và tốn kém về hiệu năng
  • Ví dụ: document.getElementById('app').innerHTML = '...' gây re-render toàn bộ subtree

Cột 2 - Virtual DOM (Đại diện của DOM bằng JavaScript):

  • Virtual DOM (VDOM) là một JavaScript object đại diện cho cấu trúc DOM
  • React giữ 2 bản VDOM: Previous VDOM (trạng thái cũ) và New VDOM (trạng thái mới sau khi state/props thay đổi)
  • VDOM rất nhẹ, chỉ là object JavaScript nên thao tác cực nhanh
  • VNode có cấu trúc:
    js:
    {
      type: 'div',
      props: { className: 'container' },
      children: [
        { type: 'h1', props: {}, children: 'Hello' },
        { type: 'p', props: {}, children: 'World' }
      ]
    }

Cột 3 - Diffing & Reconciliation (So sánh và Điều hòa):

  • React so sánh Previous VDOM với New VDOM để tìm sự khác biệt
  • Thuật toán Diffing chỉ so sánh các node cùng cấp (same level)
  • Khi phát hiện thay đổi, React tạo ra Patch (bản vá) - danh sách thay đổi tối thiểu
  • Cuối cùng, React áp dụng batch updates vào Real DOM một lần duy nhất
  • Ví dụ trong hình:
    • Patch 1: Cập nhật text của <p> từ "World" → "React"
    • Patch 2: Thêm <li> thứ 4 vào danh sách

Tại sao Virtual DOM nhanh hơn?

js:
// ❌ BAD: Thao tác trực tiếp DOM (chậm)
document.getElementById('list').innerHTML = '';
items.forEach(item => {
  const li = document.createElement('li');
  li.textContent = item;
  document.getElementById('list').appendChild(li); // Re-render mỗi lần
});

// ✅ GOOD: React Virtual DOM (nhanh)
function List({ items }) {
  return (
    <ul>
      {items.map(item => <li key={item.id}>{item.name}</li>)}
    </ul>
  );
}
// React tự động batch tất cả thay đổi và chỉ update DOM 1 lần

2. React Hooks Lifecycle

React Hooks Lifecycle

Giải thích chi tiết 3 giai đoạn:

MOUNTING PHASE (Giai đoạn khởi tạo)

Khi component lần đầu tiên được render:

  1. Component Rendered: React gọi function component lần đầu tiên
  2. useState/useReducer (Initialize State): Tạo state ban đầu
    js:
    const [count, setCount] = useState(0); // count = 0 lần đầu
  3. DOM Update: React commit VDOM thay đổi vào Real DOM
  4. useEffect (Empty Dependency Array []): Chạy sau khi DOM đã update
    js:
    useEffect(() => {
      console.log('Component mounted - chỉ chạy 1 lần');
      // Dùng để: fetch data, subscribe events, setup timers
    }, []); // [] = chỉ chạy khi mount
  5. Effects Run (After Paint): Browser đã vẽ xong UI, effects mới chạy

UPDATING PHASE (Giai đoạn cập nhật)

Khi props hoặc state thay đổi:

  1. Props or State Change: Trigger re-render
    js:
    setCount(count + 1); // State thay đổi
  2. Re-render: React gọi lại function component
  3. DOM Update: So sánh VDOM và cập nhật Real DOM
  4. useEffect Cleanup (Previous Effect): Dọn dẹp effect cũ trước
    js:
    useEffect(() => {
      const timer = setInterval(() => console.log('tick'), 1000);
      return () => clearInterval(timer); // Cleanup function
    }, [count]);
  5. useEffect (With Dependencies [deps]): Chạy effect mới nếu deps thay đổi
  6. Effects Run (After Paint): Sau khi browser vẽ UI

UNMOUNTING PHASE (Giai đoạn hủy)

Khi component bị remove khỏi DOM:

  1. Component Unmounting: React chuẩn bị xóa component
  2. useEffect Cleanup (Return Function): Dọn dẹp resources
    js:
    useEffect(() => {
      const subscription = api.subscribe();
      return () => {
        subscription.unsubscribe(); // Cleanup khi unmount
      };
    }, []);
  3. Component Removed from DOM: React xóa component khỏi Real DOM

3. Reconciliation Algorithm (Thuật toán điều hòa)

React sử dụng thuật toán Heuristic O(n) thay vì O(n³) truyền thống.

Quy tắc cốt lõi:

Quy tắc 1: So sánh theo type

js:
// Case 1: Type khác nhau → Hủy và tạo mới
// Old VDOM
<div><Counter /></div>

// New VDOM
<span><Counter /></span>

// React làm gì?
// 1. Unmount toàn bộ <div> và <Counter> cũ
// 2. Mount lại <span> và <Counter> mới (state Counter bị reset!)

Quy tắc 2: So sánh theo key

js:
// ❌ BAD: Không có key
{items.map(item => <Item data={item} />)}
// Khi xóa item đầu, React re-render TẤT CẢ items

// ✅ GOOD: Có key
{items.map(item => <Item key={item.id} data={item} />)}
// React biết chính xác item nào bị xóa/thêm, chỉ update đúng item đó

Quy tắc 3: Children Reconciliation

js:
// Case: Thêm item vào cuối danh sách
// Old: [<li key="1">A</li>, <li key="2">B</li>]
// New: [<li key="1">A</li>, <li key="2">B</li>, <li key="3">C</li>]
// React: Chỉ mount <li key="3">C</li>

// Case: Thêm item vào đầu (KHÔNG có key)
// Old: [<li>A</li>, <li>B</li>]
// New: [<li>C</li>, <li>A</li>, <li>B</li>]
// React: Re-render TẤT CẢ vì không biết đâu là C mới

4. Optimization Techniques (Kỹ thuật tối ưu)

4.1. Tránh Unnecessary Re-renders

js:
// ❌ BAD: Object/Array mới mỗi lần render
function Parent() {
  const config = { theme: 'dark' }; // Object MỚI mỗi lần render
  return <Child config={config} />; // Child bị re-render mặc dù props giống nhau
}

// ✅ GOOD: Dùng useMemo
function Parent() {
  const config = useMemo(() => ({ theme: 'dark' }), []); // Chỉ tạo 1 lần
  return <Child config={config} />; // Child không re-render
}

4.2. React.memo cho Component

js:
// ❌ BAD: Component con re-render mỗi khi Parent render
function ExpensiveChild({ data }) {
  // Heavy computation
  return <div>{data.map(...)}</div>;
}

// ✅ GOOD: Dùng React.memo
const ExpensiveChild = React.memo(function ExpensiveChild({ data }) {
  return <div>{data.map(...)}</div>;
});
// Chỉ re-render khi `data` thay đổi (shallow comparison)

4.3. useCallback cho Event Handlers

js:
// ❌ BAD: Function mới mỗi lần render
function Parent() {
  const handleClick = () => console.log('clicked'); // Function MỚI
  return <Child onClick={handleClick} />; // Child re-render
}

// ✅ GOOD: Dùng useCallback
function Parent() {
  const handleClick = useCallback(() => console.log('clicked'), []);
  return <Child onClick={handleClick} />; // Child KHÔNG re-render
}

4.4. Key Prop Pattern

js:
// ❌ BAD: Dùng index làm key
{items.map((item, index) => <Item key={index} data={item} />)}
// Khi sắp xếp/xóa item, React nhầm lẫn vì index thay đổi

// ✅ GOOD: Dùng unique ID
{items.map(item => <Item key={item.id} data={item} />)}

5. Performance Debugging

Chrome DevTools React Profiler

js:
// 1. Bật Highlight Updates trong React DevTools
// 2. Quan sát component nào re-render không cần thiết

// 3. Thêm name cho anonymous components
export default React.memo(function MyComponent() {
  // ...
});

// 4. Sử dụng Profiler API
import { Profiler } from 'react';

function onRenderCallback(id, phase, actualDuration) {
  console.log(`${id} took ${actualDuration}ms`);
}

<Profiler id="Navigation" onRender={onRenderCallback}>
  <Navigation />
</Profiler>

6. Common Mistakes (Lỗi thường gặp)

Mistake 1: Mutating State

js:
// ❌ BAD: Mutate trực tiếp
const addItem = () => {
  items.push(newItem); // Mutate array
  setItems(items); // React KHÔNG phát hiện thay đổi
};

// ✅ GOOD: Tạo array mới
const addItem = () => {
  setItems([...items, newItem]); // Immutable update
};

Mistake 2: setState trong render

js:
// ❌ BAD: setState trong render body
function Component() {
  const [count, setCount] = useState(0);
  setCount(count + 1); // Infinite loop!
  return <div>{count}</div>;
}

// ✅ GOOD: setState trong event handler hoặc useEffect
function Component() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    setCount(count + 1); // OK
  }, []);
  return <div>{count}</div>;
}

Mistake 3: Missing Dependencies

js:
// ❌ BAD: Thiếu dependencies
useEffect(() => {
  fetchData(userId); // userId KHÔNG có trong deps
}, []); // Warning: React Hook useEffect has a missing dependency

// ✅ GOOD: Đầy đủ dependencies
useEffect(() => {
  fetchData(userId);
}, [userId]); // Re-run khi userId thay đổi

7. Best Practices Checklist

  • ✅ Luôn dùng key prop cho lists
  • ✅ Tránh tạo object/array/function mới trong render
  • ✅ Dùng React.memo cho component có rendering nặng
  • ✅ Dùng useMemo cho computed values nặng
  • ✅ Dùng useCallback cho event handlers truyền xuống child
  • ✅ Tách component lớn thành components nhỏ
  • ✅ Đặt state gần nhất với nơi sử dụng
  • ✅ Sử dụng React DevTools Profiler để tìm bottleneck

Kết luận

Hiểu rõ Virtual DOM và Reconciliation giúp bạn:

  • Viết code hiệu quả hơn (tránh re-render không cần thiết)
  • Debug performance issues nhanh hơn
  • Thiết kế component architecture tốt hơn
  • Tối ưu ứng dụng cho production

Tiếp theo, hãy đọc Performance OptimizationServer Components để hiểu sâu hơn về Next.js!

Quảng cáo
mdhorizontal