Skip to content

📅 NGÀY 31: REACT RENDERING BEHAVIOR - Hiểu Cách React Re-render

📍 Thông tin khóa học

Phase 3: Complex State & Performance | Tuần 7: Performance Optimization | Ngày 31/45

⏱️ Thời lượng: 3-4 giờ (bao gồm breaks)


🎯 Mục tiêu học tập (5 phút)

Sau bài học này, bạn sẽ:

  • [ ] Hiểu rõ render cycle của React (Render phase vs Commit phase)
  • [ ] Nhận biết được khi nào và tại sao một component re-render
  • [ ] Sử dụng React DevTools Profiler để phân tích performance
  • [ ] Xác định được unnecessary re-renders trong ứng dụng
  • [ ] Áp dụng kỹ thuật đo lường và tracking render counts

🎓 Tầm quan trọng: Ngày hôm nay là nền tảng cho tuần Performance Optimization. Nếu không hiểu cách React render, bạn sẽ không biết cần optimize cái gì!


🤔 Kiểm tra đầu vào (5 phút)

Trước khi bắt đầu, hãy trả lời 3 câu hỏi sau:

1. Component nào sẽ re-render khi state thay đổi?

jsx
function Parent() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <Child1 count={count} />
      <Child2 />
    </div>
  );
}
💡 Đáp án

Cả 3 components đều re-render!

  • Parent re-render vì state thay đổi
  • Child1 re-render vì parent render
  • Child2 re-render vì parent render (dù không nhận props!)

Đây chính là điều chúng ta sẽ học hôm nay.

2. Khi nào useEffect cleanup function chạy?

💡 Đáp án
  • Trước mỗi lần effect chạy lại (nếu dependencies thay đổi)
  • Khi component unmount

Liên quan đến render cycle!

3. Bạn đã từng gặp app React chạy chậm chưa? Nguyên nhân là gì?

💡 Suy nghĩ

Thường là do:

  • Re-render quá nhiều
  • Re-render không cần thiết
  • Tính toán nặng trong render
  • Không biết cách đo lường performance

Hôm nay sẽ giải quyết tất cả!


📖 PHẦN 1: GIỚI THIỆU KHÁI NIỆM (30 phút)

1.1 Vấn Đề Thực Tế

Kịch bản: Bạn build một app Todo list đơn giản. Có 100 todos, mỗi todo là 1 component. Khi bạn gõ vào input để thêm todo mới, app bị giật lag!

jsx
function TodoApp() {
  const [todos, setTodos] = useState([
    /* 100 todos */
  ]);
  const [newTodo, setNewTodo] = useState('');

  return (
    <div>
      <input
        value={newTodo}
        onChange={(e) => setNewTodo(e.target.value)}
      />
      <div>
        {todos.map((todo) => (
          <TodoItem
            key={todo.id}
            todo={todo}
          />
        ))}
      </div>
    </div>
  );
}

Vấn đề:

  • Mỗi lần gõ 1 ký tự → newTodo state thay đổi
  • TodoApp re-render
  • TẤT CẢ 100 TodoItem components cũng re-render!
  • Dù todos không hề thay đổi!

Câu hỏi: Tại sao 100 components re-render khi chỉ có input thay đổi?


1.2 Giải Pháp: Hiểu React Rendering

React rendering hoạt động theo nguyên tắc:

"When a component renders, all of its children render too"

Khi component cha render, TẤT CẢ component con cũng render theo.

NHƯNG:

  • "Render" ≠ "Update DOM"
  • React rất thông minh trong việc update DOM
  • Vấn đề là việc TÍNH TOÁN render có thể tốn kém

Hôm nay học:

  1. Hiểu CHI TIẾT cách React render
  2. ĐO LƯỜNG performance
  3. NHẬN BIẾT vấn đề
  4. Ngày mai học GIẢI QUYẾT (React.memo, useMemo, useCallback)

1.3 Mental Model: React Render Cycle

🎬 The Two-Phase Rendering Process

USER ACTION (click, type, etc.)

STATE CHANGE

┌─────────────────────────────────────┐
│  PHASE 1: RENDER PHASE              │
│  (Pure, can be interrupted)         │
│                                     │
│  1. Call component functions        │
│  2. Execute JSX                     │
│  3. Create Virtual DOM tree         │
│  4. Compare with previous tree      │
│     (Reconciliation)                │
└─────────────────────────────────────┘

┌─────────────────────────────────────┐
│  PHASE 2: COMMIT PHASE              │
│  (Cannot be interrupted)            │
│                                     │
│  1. Apply changes to Real DOM       │
│  2. Run useLayoutEffect             │
│  3. Browser paints screen           │
│  4. Run useEffect                   │
└─────────────────────────────────────┘

USER SEES UPDATES

🧠 Analogy: Restaurant Kitchen

Render Phase = Preparing the order

  • Chef reads the order (component function)
  • Prepares ingredients (creates Virtual DOM)
  • Compares with previous order (reconciliation)
  • Can cancel if customer changes mind (can be interrupted)

Commit Phase = Serving the food

  • Put food on plate (update Real DOM)
  • Bring to customer (browser paint)
  • Cannot be undone once served (cannot be interrupted)

1.4 Hiểu Lầm Phổ Biến

❌ Hiểu lầm 1: "Render = DOM update"

SAI:

jsx
// Nhiều người nghĩ mỗi lần render = DOM update
function Counter() {
  const [count, setCount] = useState(0);
  console.log('Component rendered!'); // Chạy mỗi lần render
  return <div>{count}</div>;
}

ĐÚNG:

  • Render = Gọi component function + tạo Virtual DOM
  • DOM update = Chỉ khi Virtual DOM khác previous Virtual DOM
  • React chỉ update phần DOM thay đổi (rất hiệu quả!)

❌ Hiểu lầm 2: "Props không đổi = Không re-render"

SAI:

jsx
function Parent() {
  const [count, setCount] = useState(0);
  return <Child name='John' />; // name không đổi
}

function Child({ name }) {
  console.log('Child rendered!'); // VẪN chạy mỗi lần Parent render!
  return <div>{name}</div>;
}

ĐÚNG:

  • Khi Parent render → Child LUÔN render (mặc định)
  • Dù props không thay đổi
  • Đây là behavior mặc định của React (sẽ optimize ngày mai)

❌ Hiểu lầm 3: "setState với giá trị giống nhau = Không render"

PHỨC TẠP:

jsx
function Component() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(0); // Set cùng giá trị
  };

  console.log('Rendered');
  return <button onClick={handleClick}>Click</button>;
}

Thực tế:

  • Lần đầu click: Re-render (React kiểm tra sau)
  • Lần sau: KHÔNG re-render (React phát hiện giá trị giống nhau)
  • Gọi là "Bailout optimization"

💻 PHẦN 2: LIVE CODING (45 phút)

Demo 1: Visualizing Render Behavior ⭐

Mục tiêu: Thấy được khi nào components render

jsx
// RenderTracker.jsx
// Simple component to track renders

import { useState, useRef } from 'react';

function RenderCounter() {
  const renderCount = useRef(0);
  renderCount.current += 1;

  return (
    <span
      style={{
        background: 'yellow',
        padding: '2px 6px',
        borderRadius: '4px',
        fontSize: '12px',
      }}
    >
      Renders: {renderCount.current}
    </span>
  );
}

function Parent() {
  const [parentCount, setParentCount] = useState(0);

  console.log('🔴 Parent rendered');

  return (
    <div style={{ border: '2px solid red', padding: '20px', margin: '10px' }}>
      <h3>
        Parent <RenderCounter />
      </h3>
      <button onClick={() => setParentCount((c) => c + 1)}>
        Parent Count: {parentCount}
      </button>

      <Child1 />
      <Child2 count={parentCount} />
    </div>
  );
}

function Child1() {
  console.log('🔵 Child1 rendered');

  return (
    <div style={{ border: '2px solid blue', padding: '15px', margin: '10px' }}>
      <h4>
        Child1 (No props) <RenderCounter />
      </h4>
      <p>I don't receive any props</p>
    </div>
  );
}

function Child2({ count }) {
  console.log('🟢 Child2 rendered');

  return (
    <div style={{ border: '2px solid green', padding: '15px', margin: '10px' }}>
      <h4>
        Child2 (With props) <RenderCounter />
      </h4>
      <p>Parent count: {count}</p>
    </div>
  );
}

export default Parent;

🧪 Thí nghiệm:

  1. Click "Parent Count" button
  2. Quan sát console
  3. Quan sát render counters

📊 Kết quả:

Click 1:
🔴 Parent rendered
🔵 Child1 rendered  ← Không có props vẫn render!
🟢 Child2 rendered

Click 2:
🔴 Parent rendered
🔵 Child1 rendered  ← Vẫn render!
🟢 Child2 rendered

💡 Insight:

  • Child1 render DÙ không nhận props
  • Child2 render vì props thay đổi (hợp lý)
  • Parent render → Children render (default behavior)

Demo 2: Props Change Detection ⭐⭐

Mục tiêu: Hiểu React so sánh props như thế nào

jsx
// PropsComparisonDemo.jsx

import { useState } from 'react';

// ❌ ANTI-PATTERN: Creating new objects/arrays in render
function BadParent() {
  const [count, setCount] = useState(0);

  // 🚨 NEW object mỗi lần render!
  const user = { name: 'John', age: 30 };

  // 🚨 NEW array mỗi lần render!
  const items = ['a', 'b', 'c'];

  return (
    <div>
      <button onClick={() => setCount((c) => c + 1)}>Increment: {count}</button>
      <ChildWithObject user={user} />
      <ChildWithArray items={items} />
    </div>
  );
}

function ChildWithObject({ user }) {
  console.log('ChildWithObject rendered');
  return <div>User: {user.name}</div>;
}

function ChildWithArray({ items }) {
  console.log('ChildWithArray rendered');
  return <div>Items: {items.join(', ')}</div>;
}

// ✅ GOOD PATTERN: Stable references
function GoodParent() {
  const [count, setCount] = useState(0);

  // ✅ Defined outside component or in state/ref
  const user = useState({ name: 'John', age: 30 })[0];
  const items = useState(['a', 'b', 'c'])[0];

  return (
    <div>
      <button onClick={() => setCount((c) => c + 1)}>Increment: {count}</button>
      <ChildWithObject user={user} />
      <ChildWithArray items={items} />
    </div>
  );
}

// 🎯 DEMONSTRATION Component
export default function PropsComparisonDemo() {
  return (
    <div>
      <h3>❌ Bad: New objects every render</h3>
      <BadParent />

      <hr />

      <h3>✅ Good: Stable references</h3>
      <GoodParent />
    </div>
  );
}

🧪 So sánh:

Bad Parent:

Click button → count changes
├─ Parent renders
├─ user = NEW object (different reference)
├─ items = NEW array (different reference)
└─ Children see "different" props → render

Mỗi lần click:
- ChildWithObject renders (user reference changed)
- ChildWithArray renders (items reference changed)

Good Parent:

Click button → count changes
├─ Parent renders
├─ user = SAME object (stable reference)
├─ items = SAME array (stable reference)
└─ Children might skip render (với React.memo - ngày mai học)

NHƯNG hiện tại:
- Vẫn render vì không có optimization
- Chỉ là chuẩn bị cho ngày mai!

💡 Key Takeaway:

jsx
// React compares props using Object.is (similar to ===)

// Primitives: Compare by value
5 === 5 // true → same prop
'hello' === 'hello' // true → same prop

// Objects/Arrays: Compare by reference
{ name: 'John' } === { name: 'John' } // false → different prop!
['a', 'b'] === ['a', 'b'] // false → different prop!

const obj1 = { name: 'John' };
const obj2 = obj1;
obj1 === obj2 // true → same prop

Demo 3: State Updates & Bailout Optimization ⭐⭐⭐

Mục tiêu: React's bailout optimization khi setState giá trị giống nhau

jsx
// BailoutDemo.jsx

import { useState, useRef } from 'react';

function BailoutDemo() {
  const [count, setCount] = useState(0);
  const [user, setUser] = useState({ name: 'John' });

  const renderCount = useRef(0);
  renderCount.current += 1;

  console.log('🎨 Component rendered:', renderCount.current);

  return (
    <div style={{ padding: '20px', border: '2px solid blue' }}>
      <h3>Bailout Optimization Demo</h3>
      <p>Render count: {renderCount.current}</p>

      <hr />

      <h4>Test 1: Primitive Value (Number)</h4>
      <p>Count: {count}</p>
      <button onClick={() => setCount(0)}>Set to 0 (same value)</button>
      <button onClick={() => setCount((c) => c + 1)}>Increment</button>

      <hr />

      <h4>Test 2: Object Value</h4>
      <p>Name: {user.name}</p>

      {/* ❌ Creates NEW object → Always re-renders */}
      <button onClick={() => setUser({ name: 'John' })}>
        Set NEW object (same content)
      </button>

      {/* ✅ Same object reference → Bailout */}
      <button onClick={() => setUser(user)}>Set SAME object</button>

      {/* ✅ Different content → Re-renders */}
      <button onClick={() => setUser({ name: 'Jane' })}>
        Set different content
      </button>
    </div>
  );
}

export default BailoutDemo;

🧪 Test Cases:

Test 1: Primitive (Number)

Initial render: Render count = 1

Click "Set to 0":
├─ First time: Re-renders! (count = 2)
│  React checks AFTER render
├─ Second time: NO re-render! (count stays 2)
│  React: "0 === 0, skip render"
└─ Subsequent clicks: NO re-render

Click "Increment":
└─ Always re-renders (value actually changes)

Test 2: Object

Click "Set NEW object":
├─ ALWAYS re-renders!
│  { name: 'John' } !== { name: 'John' }
└─ Different reference → React re-renders

Click "Set SAME object":
├─ First time: Re-renders!
├─ Second time: NO re-render!
│  user === user (same reference)
└─ Bailout works!

Click "Set different content":
└─ ALWAYS re-renders (expected)

💡 Bailout Rules:

jsx
// ✅ Bailout WORKS (after first check):
setCount(0); // If count is already 0
setCount((prevCount) => prevCount); // Return same value
setUser(user); // Same object reference

// ❌ Bailout DOESN'T WORK:
setCount(0); // First time (React checks after render)
setUser({ ...user }); // New object (different reference)
setUser({ name: 'John' }); // New object (different reference)

🛠️ React DevTools Profiler

Cách sử dụng:

  1. Cài đặt: React DevTools extension (Chrome/Firefox)

  2. Mở Profiler tab:

    • F12 → React DevTools → Profiler
    • Click "Record" button (⏺️)
  3. Thực hiện actions:

    • Click buttons, type, etc.
    • Stop recording (⏹️)
  4. Phân tích:

    • Flamegraph: Thấy components nào render
    • Ranked: Components render lâu nhất
    • Timeline: Render theo thời gian

Ví dụ đọc Profiler:

Flamegraph view:

App (12ms)
├── Header (2ms)
├── Sidebar (1ms)
└── Content (9ms)
    ├── TodoList (8ms)     ← 🔥 Tốn thời gian nhất!
    │   ├── TodoItem (1ms) × 100  ← 🔥 100 items render!
    │   └── ...
    └── Footer (0.5ms)

💡 Insight: TodoList re-render 100 items mỗi lần!

Highlight Updates:

jsx
// In React DevTools → Settings
☑️ Highlight updates when components render

// Bây giờ khi component render:
// → Border màu flash xung quanh component
// → Màu xanh: Render nhanh
// → Màu vàng: Render trung bình
// → Màu đỏ: Render chậm ← 🚨 Cần optimize!

🔨 PHẦN 3: BÀI TẬP THỰC HÀNH (60 phút)

⭐ Bài 1: Tracking Renders (15 phút)

🎯 Mục tiêu: Tạo component tracking số lần render

⏱️ Thời gian: 15 phút

🚫 KHÔNG dùng: useMemo, useCallback, React.memo (chưa học)

jsx
/**
 * Requirements:
 * 1. Tạo custom hook `useRenderCount()` return số lần component render
 * 2. Sử dụng useRef để persist count across renders
 * 3. Tạo component `RenderLogger` log mỗi lần render với timestamp
 *
 * 💡 Gợi ý:
 * - useRef không trigger re-render khi update
 * - renderCount.current += 1 trong component body
 * - console.log với timestamp: new Date().toLocaleTimeString()
 */

// ❌ Cách SAI: Dùng state
function WrongRenderCount() {
  const [renderCount, setRenderCount] = useState(0);

  // 🚨 INFINITE LOOP!
  setRenderCount(renderCount + 1); // State update → Re-render → Update → ...

  return <div>Renders: {renderCount}</div>;
}

// ✅ Cách ĐÚNG: Dùng useRef
function useRenderCount() {
  const renderCount = useRef(0);

  // Safe: Không trigger re-render
  renderCount.current += 1;

  return renderCount.current;
}

// 🎯 NHIỆM VỤ CỦA BẠN:

// TODO: Implement useRenderCount hook
function useRenderCount() {
  // Your code here
}

// TODO: Implement RenderLogger component
// Should log: "Component rendered at HH:MM:SS - Render #N"
function RenderLogger({ componentName }) {
  // Your code here
  return null; // or some UI
}

// TODO: Test component
function TestComponent() {
  const [count, setCount] = useState(0);
  const renderCount = useRenderCount();

  return (
    <div>
      <RenderLogger componentName='TestComponent' />
      <p>State: {count}</p>
      <p>Renders: {renderCount}</p>
      <button onClick={() => setCount((c) => c + 1)}>Update</button>
    </div>
  );
}
✅ Solution
jsx
// useRenderCount.js
import { useRef } from 'react';

function useRenderCount() {
  const renderCount = useRef(0);
  renderCount.current += 1;
  return renderCount.current;
}

export default useRenderCount;

// RenderLogger.jsx
import { useEffect } from 'react';
import useRenderCount from './useRenderCount';

function RenderLogger({ componentName = 'Component' }) {
  const renderCount = useRenderCount();

  useEffect(() => {
    const timestamp = new Date().toLocaleTimeString();
    console.log(
      `🎨 ${componentName} rendered at ${timestamp} - Render #${renderCount}`
    );
  });

  return (
    <div style={{
      background: '#f0f0f0',
      padding: '5px',
      fontSize: '12px',
      borderRadius: '4px',
      marginBottom: '10px'
    }}>
      <strong>{componentName}</strong> - Render #{renderCount}
    </div>
  );
}

export default RenderLogger;

💡 Giải thích:

  • useRef persists value across renders WITHOUT triggering re-render
  • useEffect với no deps chạy mỗi lần render (perfect for logging)
  • Component name từ props để tái sử dụng

⭐⭐ Bài 2: Render Behavior Analysis (25 phút)

🎯 Mục tiêu: Phân tích và predict render behavior

⏱️ Thời gian: 25 phút

jsx
/**
 * Scenario: Bạn được giao một codebase cần tối ưu performance.
 * Đọc code dưới đây và trả lời câu hỏi.
 */

function App() {
  const [searchTerm, setSearchTerm] = useState('');
  const [selectedId, setSelectedId] = useState(null);

  return (
    <div>
      <SearchBar
        value={searchTerm}
        onChange={setSearchTerm}
      />
      <ProductList
        searchTerm={searchTerm}
        onSelect={setSelectedId}
      />
      <ProductDetail productId={selectedId} />
    </div>
  );
}

function SearchBar({ value, onChange }) {
  console.log('SearchBar rendered');
  return (
    <input
      value={value}
      onChange={(e) => onChange(e.target.value)}
    />
  );
}

function ProductList({ searchTerm, onSelect }) {
  console.log('ProductList rendered');
  const products = [
    /* 100 products */
  ];

  const filtered = products.filter((p) =>
    p.name.toLowerCase().includes(searchTerm.toLowerCase()),
  );

  return (
    <div>
      {filtered.map((product) => (
        <ProductItem
          key={product.id}
          product={product}
          onSelect={onSelect}
        />
      ))}
    </div>
  );
}

function ProductItem({ product, onSelect }) {
  console.log('ProductItem rendered:', product.id);
  return (
    <div onClick={() => onSelect(product.id)}>
      {product.name} - ${product.price}
    </div>
  );
}

function ProductDetail({ productId }) {
  console.log('ProductDetail rendered');
  if (!productId) return <div>Select a product</div>;

  // Fetch product details...
  return <div>Details for product {productId}</div>;
}

/**
 * 🤔 PHÂN TÍCH:
 *
 * 1. User gõ vào SearchBar. Components nào render?
 *    A. Chỉ SearchBar
 *    B. SearchBar + ProductList
 *    C. Tất cả components
 *    D. SearchBar + ProductList + ProductItems
 *
 * 2. User click vào một ProductItem. Components nào render?
 *    A. Chỉ ProductItem được click
 *    B. ProductDetail + ProductItem
 *    C. Tất cả components
 *    D. App + ProductDetail
 *
 * 3. Vấn đề performance lớn nhất là gì?
 *    A. SearchBar re-render nhiều
 *    B. ProductList filter lại mỗi lần search
 *    C. Tất cả ProductItems re-render khi search
 *    D. ProductDetail fetch data nhiều lần
 *
 * 💭 BẠN CHỌN GÌ VÀ TẠI SAO?
 * Viết phân tích của bạn:
 * - Approach hiện tại có vấn đề gì?
 * - Components nào render không cần thiết?
 * - Sẽ optimize như thế nào? (chỉ describe, chưa implement)
 */

// 🎯 NHIỆM VỤ:
// 1. Copy code trên vào editor
// 2. Thêm RenderLogger vào mỗi component
// 3. Test các scenarios:
//    - Gõ vào search (mỗi ký tự)
//    - Click vào product
//    - Clear search
// 4. Document số lần render mỗi component
// 5. Viết phân tích performance issues
✅ Solution & Analysis

Câu 1: User gõ vào SearchBarĐáp án: C - Tất cả components

User types "a":
├─ searchTerm state changes in App
├─ App renders
│  ├─ SearchBar renders (props.value changed)
│  ├─ ProductList renders (props.searchTerm changed)
│  │  ├─ ProductItem renders × N (parent renders)
│  │  └─ (Filtered array is NEW array)
│  └─ ProductDetail renders (parent renders, props.productId unchanged)

Câu 2: User click ProductItemĐáp án: C - Tất cả components

User clicks ProductItem:
├─ selectedId state changes in App
├─ App renders
│  ├─ SearchBar renders (parent renders)
│  ├─ ProductList renders (parent renders)
│  │  ├─ ProductItem renders × N (parent renders)
│  │  └─ onSelect is NEW function reference each render!
│  └─ ProductDetail renders (props.productId changed)

Câu 3: Vấn đề performanceĐáp án: C - Tất cả ProductItems re-render khi search

Phân tích chi tiết:

jsx
// Problem 1: Tất cả ProductItems re-render không cần thiết
// Mỗi lần search:
// - ProductList renders
// - filtered = NEW array (different reference)
// - 100 ProductItems render lại (dù content không đổi!)

// Problem 2: onSelect là NEW function mỗi lần
<ProductItem onSelect={onSelect} />;
// onSelect từ props là setSelectedId
// Reference không đổi NHƯNG ProductItem vẫn render vì parent render

// Problem 3: ProductList filter mỗi lần render
const filtered = products.filter(/* ... */);
// Chạy lại mỗi lần render dù searchTerm không đổi
// (VD: Click product → App renders → ProductList renders → Filter again!)

// Problem 4: ProductDetail render không cần thiết
// Khi search thay đổi, ProductDetail render dù productId không đổi

Optimization Plan (sẽ implement ngày mai):

jsx
// 1. Memo ProductItem (React.memo)
const ProductItem = React.memo(({ product, onSelect }) => {
  // Only re-render if product or onSelect actually changes
});

// 2. Memoize filtered results (useMemo)
const filtered = useMemo(
  () =>
    products.filter((p) =>
      p.name.toLowerCase().includes(searchTerm.toLowerCase()),
    ),
  [products, searchTerm],
);

// 3. Memoize callback (useCallback)
const handleSelect = useCallback((id) => {
  setSelectedId(id);
}, []); // Stable reference

// 4. Memo ProductDetail (React.memo)
const ProductDetail = React.memo(({ productId }) => {
  // Only re-render if productId changes
});

📊 Performance Impact:

BEFORE optimization:
User types "apple" (5 characters):
├─ 5 × App renders
├─ 5 × SearchBar renders
├─ 5 × ProductList renders
├─ 500 × ProductItem renders (100 items × 5 times)
└─ 5 × ProductDetail renders
Total: 515 renders for typing 5 characters!

AFTER optimization (ngày mai):
├─ 5 × App renders (necessary)
├─ 5 × SearchBar renders (necessary)
├─ 5 × ProductList renders (necessary)
├─ 20-30 × ProductItem renders (only items that changed)
└─ 0 × ProductDetail renders (productId unchanged)
Total: ~40 renders - Giảm 90%!

⭐⭐⭐ Bài 3: Performance Profiling Dashboard (40 phút)

🎯 Mục tiêu: Xây dựng dashboard để track render performance

⏱️ Thời gian: 40 phút

jsx
/**
 * 📋 Product Requirements:
 *
 * User Story:
 * "Là developer, tôi muốn có dashboard hiển thị performance metrics
 * để tôi có thể identify components render nhiều nhất"
 *
 * ✅ Acceptance Criteria:
 * - [ ] Hiển thị danh sách components và số lần render của mỗi component
 * - [ ] Highlight components render > 10 lần (màu đỏ)
 * - [ ] Button "Reset Counters" để reset tất cả counts về 0
 * - [ ] Real-time update (không cần reload page)
 * - [ ] Sắp xếp theo số lần render (nhiều nhất trước)
 *
 * 🎨 Technical Constraints:
 * - Sử dụng Context để share render counts (optional, có thể dùng props)
 * - Custom hook `useRenderTracking(componentName)` cho mỗi component
 * - Dashboard component tách biệt, có thể toggle on/off
 *
 * 🚨 Edge Cases cần handle:
 * - Component unmount (nên xóa khỏi danh sách không?)
 * - Multiple instances của cùng component name
 * - Performance của chính dashboard (không được làm chậm app!)
 */

// 🎯 NHIỆM VỤ:

// TODO 1: Create PerformanceTracker context
// Hint: Context.Provider value should contain:
// - renderCounts: { [componentName]: count }
// - trackRender: (name) => void
// - resetCounts: () => void

// TODO 2: Create useRenderTracking hook
// Hint:
// - Call trackRender in useEffect (mỗi lần render)
// - Return nothing hoặc render count

// TODO 3: Create PerformanceDashboard component
// Requirements:
// - List all components with render counts
// - Sort by count (descending)
// - Highlight if count > 10
// - Reset button

// TODO 4: Test với app có multiple components
// Create test app with:
// - Counter component (updates frequently)
// - Display component (updates less frequently)
// - Static component (rarely updates)

// 📝 Starter Code:

import { createContext, useState, useContext, useEffect, useRef } from 'react';

// TODO: Implement PerformanceContext
const PerformanceContext = createContext(null);

export function PerformanceProvider({ children }) {
  // Your code here
  return (
    <PerformanceContext.Provider value={/* ... */}>
      {children}
    </PerformanceContext.Provider>
  );
}

// TODO: Implement useRenderTracking hook
export function useRenderTracking(componentName) {
  // Your code here
}

// TODO: Implement PerformanceDashboard
export function PerformanceDashboard() {
  // Your code here
}

// TODO: Test components
function CounterComponent() {
  useRenderTracking('Counter');
  const [count, setCount] = useState(0);

  return (
    <div>
      <h3>Counter: {count}</h3>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
    </div>
  );
}

function DisplayComponent({ value }) {
  useRenderTracking('Display');
  return <div>Display: {value}</div>;
}

function StaticComponent() {
  useRenderTracking('Static');
  return <div>I am static</div>;
}

export function TestApp() {
  const [globalCount, setGlobalCount] = useState(0);

  return (
    <PerformanceProvider>
      <div>
        <PerformanceDashboard />
        <hr />
        <CounterComponent />
        <DisplayComponent value={globalCount} />
        <StaticComponent />
        <button onClick={() => setGlobalCount(c => c + 1)}>
          Update Global
        </button>
      </div>
    </PerformanceProvider>
  );
}
✅ Solution
jsx
// PerformanceTracker.jsx
import { createContext, useState, useContext, useEffect, useRef } from 'react';

// Performance Context
const PerformanceContext = createContext(null);

export function PerformanceProvider({ children }) {
  const [renderCounts, setRenderCounts] = useState({});

  const trackRender = (componentName) => {
    setRenderCounts((prev) => ({
      ...prev,
      [componentName]: (prev[componentName] || 0) + 1,
    }));
  };

  const resetCounts = () => {
    setRenderCounts({});
  };

  return (
    <PerformanceContext.Provider
      value={{ renderCounts, trackRender, resetCounts }}
    >
      {children}
    </PerformanceContext.Provider>
  );
}

// Custom hook
export function useRenderTracking(componentName) {
  const context = useContext(PerformanceContext);
  const renderCount = useRef(0);

  useEffect(() => {
    renderCount.current += 1;

    if (context) {
      context.trackRender(componentName);
    } else {
      console.warn(
        `Component "${componentName}" tracked but no PerformanceProvider found`,
      );
    }
  });

  return renderCount.current;
}

// Dashboard Component
export function PerformanceDashboard() {
  const { renderCounts, resetCounts } = useContext(PerformanceContext);
  const [isVisible, setIsVisible] = useState(true);

  // Sort by render count (descending)
  const sortedComponents = Object.entries(renderCounts).sort(
    ([, countA], [, countB]) => countB - countA,
  );

  if (!isVisible) {
    return (
      <button onClick={() => setIsVisible(true)}>
        Show Performance Dashboard
      </button>
    );
  }

  return (
    <div
      style={{
        position: 'fixed',
        top: '10px',
        right: '10px',
        background: 'white',
        border: '2px solid #333',
        borderRadius: '8px',
        padding: '15px',
        maxWidth: '300px',
        maxHeight: '400px',
        overflow: 'auto',
        boxShadow: '0 4px 6px rgba(0,0,0,0.1)',
        zIndex: 9999,
      }}
    >
      <div
        style={{
          display: 'flex',
          justifyContent: 'space-between',
          alignItems: 'center',
          marginBottom: '10px',
        }}
      >
        <h3 style={{ margin: 0, fontSize: '16px' }}>
          🎯 Performance Dashboard
        </h3>
        <button
          onClick={() => setIsVisible(false)}
          style={{
            background: 'none',
            border: 'none',
            cursor: 'pointer',
            fontSize: '18px',
          }}
        >

        </button>
      </div>

      <button
        onClick={resetCounts}
        style={{
          width: '100%',
          padding: '8px',
          marginBottom: '10px',
          cursor: 'pointer',
          borderRadius: '4px',
        }}
      >
        🔄 Reset Counters
      </button>

      {sortedComponents.length === 0 ? (
        <p style={{ color: '#666', fontSize: '14px' }}>
          No renders tracked yet
        </p>
      ) : (
        <div>
          {sortedComponents.map(([name, count]) => (
            <div
              key={name}
              style={{
                padding: '8px',
                marginBottom: '5px',
                borderRadius: '4px',
                background: count > 10 ? '#ffebee' : '#f5f5f5',
                border: count > 10 ? '1px solid #ef5350' : '1px solid #ddd',
                display: 'flex',
                justifyContent: 'space-between',
                alignItems: 'center',
              }}
            >
              <span style={{ fontSize: '14px' }}>
                {name}
                {count > 10 && ' 🔥'}
              </span>
              <span
                style={{
                  fontWeight: 'bold',
                  fontSize: '14px',
                  color: count > 10 ? '#c62828' : '#333',
                }}
              >
                {count}
              </span>
            </div>
          ))}
        </div>
      )}

      <div
        style={{
          marginTop: '10px',
          paddingTop: '10px',
          borderTop: '1px solid #ddd',
          fontSize: '12px',
          color: '#666',
        }}
      >
        <div>Total components: {sortedComponents.length}</div>
        <div>
          Total renders:{' '}
          {Object.values(renderCounts).reduce((a, b) => a + b, 0)}
        </div>
      </div>
    </div>
  );
}

// Test Components
function CounterComponent() {
  useRenderTracking('Counter');
  const [count, setCount] = useState(0);

  return (
    <div style={{ border: '1px solid #ccc', padding: '10px', margin: '10px' }}>
      <h3>Counter Component</h3>
      <p>Count: {count}</p>
      <button onClick={() => setCount((c) => c + 1)}>Increment</button>
    </div>
  );
}

function DisplayComponent({ value }) {
  useRenderTracking('Display');

  return (
    <div style={{ border: '1px solid #ccc', padding: '10px', margin: '10px' }}>
      <h3>Display Component</h3>
      <p>Value: {value}</p>
    </div>
  );
}

function StaticComponent() {
  useRenderTracking('Static');

  return (
    <div style={{ border: '1px solid #ccc', padding: '10px', margin: '10px' }}>
      <h3>Static Component</h3>
      <p>I rarely update</p>
    </div>
  );
}

// Test App
export function TestApp() {
  const [globalCount, setGlobalCount] = useState(0);

  return (
    <PerformanceProvider>
      <div style={{ padding: '20px', paddingRight: '350px' }}>
        <h1>Performance Tracking Demo</h1>

        <PerformanceDashboard />

        <div style={{ marginTop: '20px' }}>
          <button
            onClick={() => setGlobalCount((c) => c + 1)}
            style={{ padding: '10px 20px', fontSize: '16px' }}
          >
            Update Global Count: {globalCount}
          </button>
        </div>

        <CounterComponent />
        <DisplayComponent value={globalCount} />
        <StaticComponent />
      </div>
    </PerformanceProvider>
  );
}

export default TestApp;

💡 Key Concepts:

  1. Context for Global State:
jsx
// Tất cả components share same render tracking state
// Không cần props drilling
  1. useEffect without deps:
jsx
useEffect(() => {
  // Runs on EVERY render - perfect for tracking!
  trackRender(componentName);
});
  1. Sorting & Highlighting:
jsx
// Sort để thấy components render nhiều nhất
const sorted = Object.entries(counts)
  .sort(([, a], [, b]) => b - a);

// Highlight nếu > threshold
style={{ background: count > 10 ? 'red' : 'white' }}

📊 Expected Behavior:

Click "Increment" in Counter 15 times:
├─ Counter: 16 renders (initial + 15) 🔥
├─ Display: 1 render (không đổi)
└─ Static: 1 render (không đổi)

Click "Update Global" 5 times:
├─ Counter: 16 renders (không đổi)
├─ Display: 6 renders (initial + 5)
└─ Static: 6 renders (parent renders) ← Unnecessary!

⭐⭐⭐⭐ Bài 4: Render Optimization Decision Tree (60 phút)

🎯 Mục tiêu: Phân tích codebase và quyết định optimization strategy

⏱️ Thời gian: 60 phút

jsx
/**
 * 🏗️ PHASE 1: Research & Design (20 phút)
 *
 * Bạn được giao một E-commerce product page:
 * - Header: Logo, search, cart (static)
 * - ProductGallery: 5 images với thumbnails
 * - ProductInfo: Name, price, description
 * - Reviews: List of 50 customer reviews
 * - RecommendedProducts: 10 related products
 *
 * User actions:
 * - Click thumbnail → Change main image
 * - Click "Add to Cart" → Update cart count
 * - Scroll reviews → Load more
 *
 * Nhiệm vụ:
 * 1. Vẽ component tree
 * 2. Identify which components render when:
 *    a. User clicks thumbnail
 *    b. User adds to cart
 *    c. User loads more reviews
 * 3. List unnecessary re-renders
 * 4. Propose optimization strategy (chỉ describe, chưa code)
 *
 * ADR Template:
 *
 * ## Context
 * - Current performance issue: [Mô tả]
 * - Impact: [Users experience...]
 * - Measurement: [Current render counts...]
 *
 * ## Decision
 * - Components to optimize: [List]
 * - Techniques to apply: [React.memo, useMemo, useCallback]
 *
 * ## Rationale
 * Why optimize these specific components:
 * 1. [Component A]: Because...
 * 2. [Component B]: Because...
 *
 * ## Consequences
 * Accepted trade-offs:
 * - More complex code (memoization logic)
 * - Slightly more memory (memoized values)
 * - Better UX (faster interactions)
 *
 * ## Alternatives Considered
 * - Option 1: [Describe]
 *   Pros: ...
 *   Cons: ...
 * - Option 2: [Describe]
 *   Pros: ...
 *   Cons: ...
 */

// 💻 PHASE 2: Implementation (30 phút)

// Current implementation (có performance issues):

function ProductPage() {
  const [selectedImage, setSelectedImage] = useState(0);
  const [cartCount, setCartCount] = useState(0);
  const [reviewPage, setReviewPage] = useState(1);

  const product = {
    name: 'Premium Headphones',
    price: 299,
    description: 'High-quality wireless headphones...',
    images: [
      /* 5 image URLs */
    ],
    reviews: [
      /* 50 reviews */
    ],
  };

  const relatedProducts = [
    /* 10 products */
  ];

  const handleAddToCart = () => {
    setCartCount((prev) => prev + 1);
    // API call...
  };

  const loadMoreReviews = () => {
    setReviewPage((prev) => prev + 1);
    // Fetch more reviews...
  };

  return (
    <div>
      <Header cartCount={cartCount} />

      <ProductGallery
        images={product.images}
        selectedIndex={selectedImage}
        onSelectImage={setSelectedImage}
      />

      <ProductInfo
        name={product.name}
        price={product.price}
        description={product.description}
        onAddToCart={handleAddToCart}
      />

      <Reviews
        reviews={product.reviews}
        page={reviewPage}
        onLoadMore={loadMoreReviews}
      />

      <RecommendedProducts products={relatedProducts} />
    </div>
  );
}

function Header({ cartCount }) {
  console.log('Header rendered');
  return (
    <header>
      <Logo />
      <SearchBar />
      <CartIcon count={cartCount} />
    </header>
  );
}

function Logo() {
  console.log('Logo rendered');
  return (
    <img
      src='/logo.png'
      alt='Logo'
    />
  );
}

function SearchBar() {
  console.log('SearchBar rendered');
  const [query, setQuery] = useState('');
  return (
    <input
      value={query}
      onChange={(e) => setQuery(e.target.value)}
    />
  );
}

function CartIcon({ count }) {
  console.log('CartIcon rendered');
  return <div>🛒 {count}</div>;
}

function ProductGallery({ images, selectedIndex, onSelectImage }) {
  console.log('ProductGallery rendered');
  return (
    <div>
      <MainImage src={images[selectedIndex]} />
      <Thumbnails
        images={images}
        selectedIndex={selectedIndex}
        onSelect={onSelectImage}
      />
    </div>
  );
}

function MainImage({ src }) {
  console.log('MainImage rendered');
  return (
    <img
      src={src}
      alt='Product'
      style={{ width: '500px' }}
    />
  );
}

function Thumbnails({ images, selectedIndex, onSelect }) {
  console.log('Thumbnails rendered');
  return (
    <div>
      {images.map((img, index) => (
        <Thumbnail
          key={index}
          src={img}
          isSelected={index === selectedIndex}
          onClick={() => onSelect(index)}
        />
      ))}
    </div>
  );
}

function Thumbnail({ src, isSelected, onClick }) {
  console.log('Thumbnail rendered:', src);
  return (
    <img
      src={src}
      alt='Thumbnail'
      style={{
        width: '100px',
        border: isSelected ? '2px solid blue' : 'none',
        cursor: 'pointer',
      }}
      onClick={onClick}
    />
  );
}

function ProductInfo({ name, price, description, onAddToCart }) {
  console.log('ProductInfo rendered');
  return (
    <div>
      <h1>{name}</h1>
      <p>${price}</p>
      <p>{description}</p>
      <button onClick={onAddToCart}>Add to Cart</button>
    </div>
  );
}

function Reviews({ reviews, page, onLoadMore }) {
  console.log('Reviews rendered');
  const displayedReviews = reviews.slice(0, page * 10);

  return (
    <div>
      <h2>Customer Reviews</h2>
      {displayedReviews.map((review) => (
        <ReviewItem
          key={review.id}
          review={review}
        />
      ))}
      {displayedReviews.length < reviews.length && (
        <button onClick={onLoadMore}>Load More</button>
      )}
    </div>
  );
}

function ReviewItem({ review }) {
  console.log('ReviewItem rendered:', review.id);
  return (
    <div>
      <strong>{review.author}</strong>
      <p>{review.text}</p>
      <div>Rating: {'⭐'.repeat(review.rating)}</div>
    </div>
  );
}

function RecommendedProducts({ products }) {
  console.log('RecommendedProducts rendered');
  return (
    <div>
      <h2>You May Also Like</h2>
      {products.map((product) => (
        <ProductCard
          key={product.id}
          product={product}
        />
      ))}
    </div>
  );
}

function ProductCard({ product }) {
  console.log('ProductCard rendered:', product.id);
  return (
    <div>
      <img
        src={product.image}
        alt={product.name}
      />
      <h3>{product.name}</h3>
      <p>${product.price}</p>
    </div>
  );
}

/**
 * 🎯 NHIỆM VỤ:
 *
 * 1. Sử dụng useRenderTracking cho tất cả components
 * 2. Test 3 scenarios và ghi lại số lần render:
 *    - Scenario A: Click 5 thumbnails
 *    - Scenario B: Add to cart 3 times
 *    - Scenario C: Load more reviews 2 times
 * 3. Identify top 5 components với unnecessary renders
 * 4. Viết optimization plan (sẽ implement ngày mai):
 *    - Which components cần React.memo?
 *    - Which values cần useMemo?
 *    - Which callbacks cần useCallback?
 */

// 🧪 PHASE 3: Testing (10 phút)
// Test với React DevTools Profiler và verify:
// - [ ] Identified all unnecessary renders
// - [ ] Prioritized optimizations by impact
// - [ ] Documented expected improvements
✅ Solution & Analysis

📊 PHASE 1: Analysis Results

Component Tree:

ProductPage
├── Header
│   ├── Logo
│   ├── SearchBar
│   └── CartIcon
├── ProductGallery
│   ├── MainImage
│   └── Thumbnails
│       └── Thumbnail × 5
├── ProductInfo
├── Reviews
│   └── ReviewItem × (page * 10)
└── RecommendedProducts
    └── ProductCard × 10

Render Analysis:

Scenario A: Click thumbnail (5 times)

Click thumbnail 1:
✅ ProductPage (state: selectedImage)
✅ ProductGallery (props: selectedIndex changed)
✅ MainImage (props: src changed)
✅ Thumbnails (props: selectedIndex changed)
✅ Thumbnail × 5 (props: isSelected changed for 2 items)

❌ Header (parent renders) - UNNECESSARY!
❌ Logo (parent renders) - UNNECESSARY!
❌ SearchBar (parent renders) - UNNECESSARY!
❌ CartIcon (props unchanged) - UNNECESSARY!
❌ ProductInfo (props unchanged) - UNNECESSARY!
❌ Reviews (props unchanged) - UNNECESSARY!
❌ ReviewItem × N (parent renders) - UNNECESSARY!
❌ RecommendedProducts (props unchanged) - UNNECESSARY!
❌ ProductCard × 10 (parent renders) - UNNECESSARY!

Total: ~30 renders
Necessary: ~7 renders
Wasted: ~23 renders (77%)!

Scenario B: Add to cart (3 times)

Click "Add to Cart":
✅ ProductPage (state: cartCount)
✅ Header (props: cartCount changed)
✅ CartIcon (props: count changed)

❌ Logo (parent renders) - UNNECESSARY!
❌ SearchBar (parent renders) - UNNECESSARY!
❌ ProductGallery (props unchanged) - UNNECESSARY!
❌ MainImage (parent renders) - UNNECESSARY!
❌ Thumbnails (parent renders) - UNNECESSARY!
❌ Thumbnail × 5 (parent renders) - UNNECESSARY!
❌ ProductInfo (props unchanged but onAddToCart is NEW function!) - UNNECESSARY!
❌ Reviews - UNNECESSARY!
❌ RecommendedProducts - UNNECESSARY!

Total: ~25 renders
Necessary: ~3 renders
Wasted: ~22 renders (88%)!

Scenario C: Load more reviews (2 times)

Click "Load More":
✅ ProductPage (state: reviewPage)
✅ Reviews (props: page changed)
✅ ReviewItem × 10 (NEW items added)

❌ ALL other components - UNNECESSARY!

Total: ~30 renders
Necessary: ~12 renders
Wasted: ~18 renders (60%)

ADR (Architecture Decision Record):

markdown
# ADR-001: Product Page Performance Optimization

## Context

Current State:

- Product page renders 25-30 components on EVERY state change
- 60-88% of renders are unnecessary (components with unchanged props)
- User interactions feel sluggish (>100ms response time)
- Particularly bad when clicking thumbnails repeatedly

Impact:

- Poor UX on mobile devices (limited CPU)
- High battery consumption
- Frustrated users (reported in support tickets)

Measurements:

- Thumbnail click: 23/30 renders wasted (77%)
- Add to cart: 22/25 renders wasted (88%)
- Load reviews: 18/30 renders wasted (60%)
- Average: 21/28 renders wasted (75%)

## Decision

We will implement selective memoization using:

1. React.memo for leaf components
2. useMemo for expensive computations
3. useCallback for event handlers passed to memoized children

Priority Order (by impact):

1. HIGH: Memo components with many children (Header, ProductGallery, Reviews)
2. MEDIUM: Memo leaf components (Logo, ReviewItem, ProductCard)
3. LOW: Memoize callbacks and computed values

## Rationale

**Components to Optimize:**

1. **Header + children (HIGH)**
   - Why: 4 components render on EVERY action
   - Impact: cartCount only changes occasionally
   - Solution: React.memo on Header, Logo, SearchBar

2. **ProductGallery + Thumbnails (HIGH)**
   - Why: 7 components render when cart/reviews update
   - Impact: Expensive image rendering
   - Solution: React.memo + useCallback for onSelectImage

3. **Reviews + ReviewItem × N (HIGH)**
   - Why: All N items re-render on unrelated actions
   - Impact: N can be 50+ (expensive!)
   - Solution: React.memo on ReviewItem

4. **RecommendedProducts + Cards (MEDIUM)**
   - Why: 11 components always re-render
   - Impact: Less critical (below fold)
   - Solution: React.memo on both

5. **ProductInfo (LOW)**
   - Why: Simple component, cheap to render
   - Impact: Minimal performance gain
   - Solution: Maybe skip or useCallback for button

**NOT Optimizing:**

- ProductPage: Must render (state holder)
- MainImage: Props actually change
- CartIcon: Props actually change

## Consequences

**Accepted Trade-offs:**

✅ Pros:

- 75% reduction in wasted renders (21 → ~5 renders)
- Faster user interactions (<16ms response)
- Better mobile performance
- Improved user satisfaction

❌ Cons:

- Slightly more complex code (memo wrappers)
- Need to maintain callback stability (useCallback)
- More memory usage (memoized values)
- Developer must understand memoization

**Risks Mitigated:**

- Over-optimization: Only memo components that actually re-render unnecessarily
- Premature optimization: Have measurements first
- Incorrect deps: Will use ESLint exhaustive-deps

## Alternatives Considered

**Option 1: Component Splitting**

```jsx
// Split ProductPage into multiple state containers
function ProductPage() {
  return (
    <>
      <GalleryContainer />
      <CartContainer />
      <ReviewsContainer />
    </>
  );
}
```

Pros:

  • Isolates state changes
  • No memoization needed Cons:
  • Need Context for shared state
  • More boilerplate
  • Harder to understand data flow

Decision: Rejected. Too much refactoring for this page.

Option 2: Virtual Scrolling for Reviews

jsx
// Use react-window for long lists
import { FixedSizeList } from 'react-window';

Pros:

  • Only renders visible items
  • Great for very long lists Cons:
  • External dependency
  • Overkill for 50 items
  • More complex implementation

Decision: Consider for future if reviews > 100.

Option 3: No Optimization

Do nothing, accept current performance

Pros:

  • Simplest code
  • No maintenance burden Cons:
  • Poor UX
  • User complaints
  • Competitive disadvantage

Decision: Rejected. Must optimize.

markdown
## Implementation Plan

**Phase 1 (Day 32):** Learn React.memo

- Implement on simple components (Logo, CartIcon)
- Verify with DevTools Profiler

**Phase 2 (Day 33):** Learn useMemo

- Optimize expensive computations
- Reviews pagination calculation

**Phase 3 (Day 34):** Learn useCallback

- Stabilize event handlers
- Apply to all memoized components

**Phase 4 (Day 35):** Integration

- Apply all optimizations to ProductPage
- Measure improvements
- Document lessons learned

## Success Metrics

Before:

- Avg renders per action: 28
- Wasted renders: 21 (75%)
- Response time: 120ms

Target After:

- Avg renders per action: 7
- Wasted renders: <2 (< 30%)
- Response time: <16ms

## Status

- [x] Analysis completed (Day 31)
- [ ] React.memo learned (Day 32)
- [ ] useMemo learned (Day 33)
- [ ] useCallback learned (Day 34)
- [ ] Full optimization (Day 35)

**📝 Optimization Plan Summary:**

```jsx
// HIGH PRIORITY

// 1. Header tree
const Header = React.memo(({ cartCount }) => {
  /* ... */
});
const Logo = React.memo(() => {
  /* ... */
});
const SearchBar = React.memo(() => {
  /* ... */
});

// 2. Gallery tree
const ProductGallery = React.memo(
  ({ images, selectedIndex, onSelectImage }) => {
    // onSelectImage MUST be stable (useCallback)
  },
);
const Thumbnail = React.memo(({ src, isSelected, onClick }) => {
  // onClick MUST be stable
});

// 3. Reviews tree
const Reviews = React.memo(({ reviews, page, onLoadMore }) => {
  // displayedReviews should use useMemo
  const displayedReviews = useMemo(
    () => reviews.slice(0, page * 10),
    [reviews, page],
  );
});
const ReviewItem = React.memo(({ review }) => {
  /* ... */
});

// MEDIUM PRIORITY

// 4. Recommended products
const RecommendedProducts = React.memo(({ products }) => {
  /* ... */
});
const ProductCard = React.memo(({ product }) => {
  /* ... */
});

// IN ProductPage:

// Stable callbacks
const handleAddToCart = useCallback(() => {
  setCartCount((prev) => prev + 1);
}, []);

const handleSelectImage = useCallback((index) => {
  setSelectedImage(index);
}, []);

const loadMoreReviews = useCallback(() => {
  setReviewPage((prev) => prev + 1);
}, []);
```

Expected Improvements:

After Optimization:

Scenario A (Click thumbnail):
├─ ProductPage (necessary)
├─ ProductGallery (necessary)
├─ MainImage (necessary)
├─ Thumbnails (necessary)
├─ Thumbnail × 2 (only changed ones)
└─ Total: 6 renders (was 30) - 80% reduction!

Scenario B (Add to cart):
├─ ProductPage (necessary)
├─ Header (necessary)
├─ CartIcon (necessary)
└─ Total: 3 renders (was 25) - 88% reduction!

Scenario C (Load reviews):
├─ ProductPage (necessary)
├─ Reviews (necessary)
├─ ReviewItem × 10 (new items)
└─ Total: 12 renders (was 30) - 60% reduction!

Overall: 7 average renders (was 28) - 75% reduction!

⭐⭐⭐⭐⭐ Bài 5: Production-Ready Render Performance Monitor (90 phút)

🎯 Mục tiêu: Xây dựng công cụ monitoring có thể dùng trong production

⏱️ Thời gian: 90 phút

jsx
/**
 * 📋 Feature Specification:
 *
 * Build a developer tool that:
 * 1. Tracks render performance in development
 * 2. Can be toggled on/off via keyboard shortcut
 * 3. Shows real-time metrics
 * 4. Exports performance report
 * 5. Warns about performance issues
 *
 * Requirements:
 * - Minimal performance impact (< 1ms overhead)
 * - Only active in development mode
 * - Persist settings to localStorage
 * - Keyboard shortcuts (Ctrl+Shift+P to toggle)
 * - Export to JSON/CSV
 */

// 🏗️ Technical Design Doc:

/**
 * 1. Component Architecture:
 *
 * PerformanceMonitor (root)
 * ├── MonitorProvider (context)
 * ├── MonitorDashboard (UI)
 * │   ├── MetricsPanel
 * │   ├── ComponentList
 * │   ├── WarningsPanel
 * │   └── ExportButton
 * └── usePerformanceTracking (hook)
 *
 * 2. State Management:
 *
 * State structure:
 * {
 *   isEnabled: boolean,
 *   metrics: {
 *     [componentName]: {
 *       renderCount: number,
 *       totalTime: number,
 *       avgTime: number,
 *       lastRenderTime: number,
 *       firstRender: timestamp,
 *       lastRender: timestamp
 *     }
 *   },
 *   warnings: Array<{
 *     componentName: string,
 *     type: 'high_count' | 'slow_render',
 *     value: number,
 *     timestamp: timestamp
 *   }>
 * }
 *
 * 3. Performance Thresholds:
 *
 * - HIGH_RENDER_COUNT: 10 renders
 * - SLOW_RENDER_TIME: 16ms
 * - WARNING_RETENTION: 50 warnings max
 *
 * 4. Features:
 *
 * ✅ Real-time tracking
 * ✅ Keyboard toggle (Ctrl+Shift+P)
 * ✅ Performance warnings
 * ✅ Export to JSON/CSV
 * ✅ LocalStorage persistence
 * ✅ Dev-only mode
 *
 * 5. Edge Cases:
 *
 * - Component unmount → Keep data or clear?
 * - Very fast renders (< 1ms) → Still track
 * - Many warnings → Limit to 50, FIFO
 * - localStorage full → Graceful degradation
 * - Production build → Completely disabled
 */

// ✅ Production Checklist:

/**
 * - [ ] TypeScript types complete
 * - [ ] Unit tests (coverage > 80%)
 * - [ ] Integration tests
 * - [ ] Error boundaries
 * - [ ] Loading states (N/A for this feature)
 * - [ ] Error states (localStorage failure)
 * - [ ] A11y: Keyboard navigation, ARIA labels
 * - [ ] Performance: < 1ms overhead
 * - [ ] SEO: N/A
 * - [ ] Security: No sensitive data logged
 * - [ ] Mobile responsive
 * - [ ] Cross-browser (Chrome, Firefox, Safari)
 */

// 🎯 IMPLEMENTATION:

// TODO 1: MonitorContext với advanced state management
// TODO 2: Performance measuring với high-precision timing
// TODO 3: Warning system với thresholds
// TODO 4: Export functionality (JSON + CSV)
// TODO 5: Keyboard shortcuts
// TODO 6: localStorage persistence
// TODO 7: Production guard (process.env.NODE_ENV)

// Starter code provided below...
📝 Full Implementation Code
jsx
// PerformanceMonitor.jsx
import {
  createContext,
  useContext,
  useState,
  useEffect,
  useRef,
  useCallback,
} from 'react';

// ============================================
// CONSTANTS & TYPES
// ============================================

const THRESHOLDS = {
  HIGH_RENDER_COUNT: 10,
  SLOW_RENDER_TIME: 16, // ms
  WARNING_RETENTION: 50,
};

const STORAGE_KEY = 'react-performance-monitor';

const IS_DEV = process.env.NODE_ENV === 'development';

// ============================================
// CONTEXT
// ============================================

const MonitorContext = createContext(null);

export function PerformanceMonitorProvider({ children }) {
  const [isEnabled, setIsEnabled] = useState(() => {
    if (!IS_DEV) return false;
    try {
      const stored = localStorage.getItem(STORAGE_KEY);
      return stored ? JSON.parse(stored).isEnabled : false;
    } catch {
      return false;
    }
  });

  const [metrics, setMetrics] = useState({});
  const [warnings, setWarnings] = useState([]);

  // Persist enabled state
  useEffect(() => {
    if (!IS_DEV) return;
    try {
      localStorage.setItem(
        STORAGE_KEY,
        JSON.stringify({ isEnabled, timestamp: Date.now() }),
      );
    } catch (err) {
      console.warn('Failed to persist monitor state:', err);
    }
  }, [isEnabled]);

  // Keyboard shortcut: Ctrl+Shift+P
  useEffect(() => {
    if (!IS_DEV) return;

    const handleKeyDown = (e) => {
      if (e.ctrlKey && e.shiftKey && e.key === 'P') {
        e.preventDefault();
        setIsEnabled((prev) => !prev);
        console.log('🎯 Performance Monitor:', !isEnabled ? 'ON' : 'OFF');
      }
    };

    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [isEnabled]);

  const trackRender = useCallback(
    (componentName, renderTime) => {
      if (!isEnabled) return;

      setMetrics((prev) => {
        const existing = prev[componentName] || {
          renderCount: 0,
          totalTime: 0,
          avgTime: 0,
          lastRenderTime: 0,
          firstRender: Date.now(),
          lastRender: Date.now(),
        };

        const newCount = existing.renderCount + 1;
        const newTotalTime = existing.totalTime + renderTime;
        const newAvgTime = newTotalTime / newCount;

        // Check for warnings
        if (newCount > THRESHOLDS.HIGH_RENDER_COUNT) {
          setWarnings((prevWarnings) => {
            const newWarning = {
              componentName,
              type: 'high_count',
              value: newCount,
              timestamp: Date.now(),
            };

            const updated = [...prevWarnings, newWarning];
            return updated.slice(-THRESHOLDS.WARNING_RETENTION);
          });
        }

        if (renderTime > THRESHOLDS.SLOW_RENDER_TIME) {
          setWarnings((prevWarnings) => {
            const newWarning = {
              componentName,
              type: 'slow_render',
              value: renderTime,
              timestamp: Date.now(),
            };

            const updated = [...prevWarnings, newWarning];
            return updated.slice(-THRESHOLDS.WARNING_RETENTION);
          });
        }

        return {
          ...prev,
          [componentName]: {
            renderCount: newCount,
            totalTime: newTotalTime,
            avgTime: newAvgTime,
            lastRenderTime: renderTime,
            firstRender: existing.firstRender,
            lastRender: Date.now(),
          },
        };
      });
    },
    [isEnabled],
  );

  const resetMetrics = useCallback(() => {
    setMetrics({});
    setWarnings([]);
  }, []);

  const exportData = useCallback(
    (format = 'json') => {
      const data = {
        exportedAt: new Date().toISOString(),
        metrics,
        warnings,
        summary: {
          totalComponents: Object.keys(metrics).length,
          totalRenders: Object.values(metrics).reduce(
            (sum, m) => sum + m.renderCount,
            0,
          ),
          totalWarnings: warnings.length,
        },
      };

      if (format === 'json') {
        const blob = new Blob([JSON.stringify(data, null, 2)], {
          type: 'application/json',
        });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = `performance-report-${Date.now()}.json`;
        a.click();
        URL.revokeObjectURL(url);
      } else if (format === 'csv') {
        const rows = [
          [
            'Component',
            'Renders',
            'Avg Time (ms)',
            'Total Time (ms)',
            'Last Render (ms)',
          ],
          ...Object.entries(metrics).map(([name, m]) => [
            name,
            m.renderCount,
            m.avgTime.toFixed(2),
            m.totalTime.toFixed(2),
            m.lastRenderTime.toFixed(2),
          ]),
        ];

        const csv = rows.map((row) => row.join(',')).join('\n');
        const blob = new Blob([csv], { type: 'text/csv' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = `performance-report-${Date.now()}.csv`;
        a.click();
        URL.revokeObjectURL(url);
      }
    },
    [metrics, warnings],
  );

  const value = {
    isEnabled,
    setIsEnabled,
    metrics,
    warnings,
    trackRender,
    resetMetrics,
    exportData,
  };

  return (
    <MonitorContext.Provider value={value}>{children}</MonitorContext.Provider>
  );
}

// ============================================
// HOOK
// ============================================

export function usePerformanceTracking(componentName) {
  const context = useContext(MonitorContext);
  const startTimeRef = useRef(0);

  useEffect(() => {
    if (!IS_DEV || !context || !context.isEnabled) return;

    // Measure render time
    const renderTime = performance.now() - startTimeRef.current;
    context.trackRender(componentName, renderTime);
  });

  // Record start time BEFORE render
  startTimeRef.current = performance.now();
}

// ============================================
// DASHBOARD COMPONENT
// ============================================

export function PerformanceMonitorDashboard() {
  const context = useContext(MonitorContext);

  if (!IS_DEV || !context) {
    return null;
  }

  const {
    isEnabled,
    setIsEnabled,
    metrics,
    warnings,
    resetMetrics,
    exportData,
  } = context;

  const [isVisible, setIsVisible] = useState(true);

  if (!isEnabled) {
    return (
      <div
        style={{
          position: 'fixed',
          bottom: '20px',
          right: '20px',
          zIndex: 9999,
        }}
      >
        <button
          onClick={() => setIsEnabled(true)}
          style={{
            padding: '10px 15px',
            background: '#333',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            cursor: 'pointer',
            fontSize: '14px',
          }}
        >
          🎯 Enable Monitor (Ctrl+Shift+P)
        </button>
      </div>
    );
  }

  if (!isVisible) {
    return (
      <div
        style={{
          position: 'fixed',
          bottom: '20px',
          right: '20px',
          zIndex: 9999,
        }}
      >
        <button
          onClick={() => setIsVisible(true)}
          style={{
            padding: '10px 15px',
            background: '#4caf50',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            cursor: 'pointer',
            fontSize: '14px',
          }}
        >
          📊 Show Dashboard
        </button>
      </div>
    );
  }

  const sortedMetrics = Object.entries(metrics).sort(
    ([, a], [, b]) => b.renderCount - a.renderCount,
  );

  const totalRenders = Object.values(metrics).reduce(
    (sum, m) => sum + m.renderCount,
    0,
  );

  const slowComponents = sortedMetrics.filter(
    ([, m]) => m.avgTime > THRESHOLDS.SLOW_RENDER_TIME,
  );

  const frequentComponents = sortedMetrics.filter(
    ([, m]) => m.renderCount > THRESHOLDS.HIGH_RENDER_COUNT,
  );

  return (
    <div
      style={{
        position: 'fixed',
        top: '20px',
        right: '20px',
        width: '400px',
        maxHeight: '80vh',
        overflow: 'auto',
        background: 'white',
        border: '2px solid #333',
        borderRadius: '8px',
        padding: '15px',
        boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
        zIndex: 9999,
        fontFamily: 'monospace',
        fontSize: '13px',
      }}
    >
      {/* Header */}
      <div
        style={{
          display: 'flex',
          justifyContent: 'space-between',
          alignItems: 'center',
          marginBottom: '15px',
          paddingBottom: '10px',
          borderBottom: '2px solid #eee',
        }}
      >
        <h3 style={{ margin: 0, fontSize: '16px' }}>🎯 Performance Monitor</h3>
        <div>
          <button
            onClick={() => setIsVisible(false)}
            style={{
              background: 'none',
              border: 'none',
              cursor: 'pointer',
              fontSize: '18px',
              marginLeft: '10px',
            }}
            title='Minimize'
          >

          </button>
          <button
            onClick={() => setIsEnabled(false)}
            style={{
              background: 'none',
              border: 'none',
              cursor: 'pointer',
              fontSize: '18px',
              marginLeft: '5px',
            }}
            title='Close (Ctrl+Shift+P)'
          >

          </button>
        </div>
      </div>

      {/* Summary */}
      <div
        style={{
          background: '#f5f5f5',
          padding: '10px',
          borderRadius: '4px',
          marginBottom: '15px',
        }}
      >
        <div
          style={{
            display: 'grid',
            gridTemplateColumns: '1fr 1fr',
            gap: '10px',
          }}
        >
          <div>
            <div style={{ color: '#666', fontSize: '11px' }}>
              Total Components
            </div>
            <div style={{ fontSize: '20px', fontWeight: 'bold' }}>
              {Object.keys(metrics).length}
            </div>
          </div>
          <div>
            <div style={{ color: '#666', fontSize: '11px' }}>Total Renders</div>
            <div style={{ fontSize: '20px', fontWeight: 'bold' }}>
              {totalRenders}
            </div>
          </div>
          <div>
            <div style={{ color: '#666', fontSize: '11px' }}>Warnings</div>
            <div
              style={{ fontSize: '20px', fontWeight: 'bold', color: '#f44336' }}
            >
              {warnings.length}
            </div>
          </div>
          <div>
            <div style={{ color: '#666', fontSize: '11px' }}>
              Slow Components
            </div>
            <div
              style={{ fontSize: '20px', fontWeight: 'bold', color: '#ff9800' }}
            >
              {slowComponents.length}
            </div>
          </div>
        </div>
      </div>

      {/* Warnings */}
      {warnings.length > 0 && (
        <div style={{ marginBottom: '15px' }}>
          <h4 style={{ margin: '0 0 10px 0', fontSize: '14px' }}>
            ⚠️ Recent Warnings
          </h4>
          <div style={{ maxHeight: '150px', overflow: 'auto' }}>
            {warnings
              .slice(-5)
              .reverse()
              .map((warning, idx) => (
                <div
                  key={idx}
                  style={{
                    padding: '8px',
                    background:
                      warning.type === 'slow_render' ? '#fff3e0' : '#ffebee',
                    border: `1px solid ${warning.type === 'slow_render' ? '#ff9800' : '#f44336'}`,
                    borderRadius: '4px',
                    marginBottom: '5px',
                    fontSize: '12px',
                  }}
                >
                  <div style={{ fontWeight: 'bold' }}>
                    {warning.componentName}
                  </div>
                  <div style={{ color: '#666' }}>
                    {warning.type === 'slow_render'
                      ? `Slow render: ${warning.value.toFixed(2)}ms`
                      : `High count: ${warning.value} renders`}
                  </div>
                  <div style={{ color: '#999', fontSize: '11px' }}>
                    {new Date(warning.timestamp).toLocaleTimeString()}
                  </div>
                </div>
              ))}
          </div>
        </div>
      )}

      {/* Component List */}
      <div style={{ marginBottom: '15px' }}>
        <h4 style={{ margin: '0 0 10px 0', fontSize: '14px' }}>
          📊 Components
        </h4>
        <div style={{ maxHeight: '300px', overflow: 'auto' }}>
          {sortedMetrics.map(([name, m]) => {
            const isWarning = m.renderCount > THRESHOLDS.HIGH_RENDER_COUNT;
            const isSlow = m.avgTime > THRESHOLDS.SLOW_RENDER_TIME;

            return (
              <div
                key={name}
                style={{
                  padding: '10px',
                  background: isWarning || isSlow ? '#fff3e0' : '#f9f9f9',
                  border: `1px solid ${isWarning || isSlow ? '#ff9800' : '#ddd'}`,
                  borderRadius: '4px',
                  marginBottom: '8px',
                }}
              >
                <div
                  style={{
                    display: 'flex',
                    justifyContent: 'space-between',
                    marginBottom: '5px',
                  }}
                >
                  <strong>{name}</strong>
                  <span style={{ color: isWarning ? '#f44336' : '#333' }}>
                    {m.renderCount} renders
                  </span>
                </div>
                <div
                  style={{
                    display: 'grid',
                    gridTemplateColumns: '1fr 1fr 1fr',
                    gap: '5px',
                    fontSize: '11px',
                    color: '#666',
                  }}
                >
                  <div>
                    Avg:{' '}
                    <strong style={{ color: isSlow ? '#ff9800' : '#333' }}>
                      {m.avgTime.toFixed(2)}ms
                    </strong>
                  </div>
                  <div>
                    Last: <strong>{m.lastRenderTime.toFixed(2)}ms</strong>
                  </div>
                  <div>
                    Total: <strong>{m.totalTime.toFixed(2)}ms</strong>
                  </div>
                </div>
              </div>
            );
          })}
        </div>
      </div>

      {/* Actions */}
      <div style={{ display: 'flex', gap: '10px', flexWrap: 'wrap' }}>
        <button
          onClick={resetMetrics}
          style={{
            flex: 1,
            padding: '8px',
            background: '#f44336',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            cursor: 'pointer',
            fontSize: '12px',
          }}
        >
          🔄 Reset
        </button>
        <button
          onClick={() => exportData('json')}
          style={{
            flex: 1,
            padding: '8px',
            background: '#2196f3',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            cursor: 'pointer',
            fontSize: '12px',
          }}
        >
          📥 Export JSON
        </button>
        <button
          onClick={() => exportData('csv')}
          style={{
            flex: 1,
            padding: '8px',
            background: '#4caf50',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            cursor: 'pointer',
            fontSize: '12px',
          }}
        >
          📊 Export CSV
        </button>
      </div>

      {/* Footer */}
      <div
        style={{
          marginTop: '15px',
          paddingTop: '10px',
          borderTop: '1px solid #eee',
          fontSize: '11px',
          color: '#999',
          textAlign: 'center',
        }}
      >
        Press <kbd>Ctrl+Shift+P</kbd> to toggle monitor
      </div>
    </div>
  );
}

// ============================================
// EXAMPLE USAGE
// ============================================

// In your app root:
/*
function App() {
  return (
    <PerformanceMonitorProvider>
      <YourApp />
      <PerformanceMonitorDashboard />
    </PerformanceMonitorProvider>
  );
}

// In components you want to track:
function MyComponent() {
  usePerformanceTracking('MyComponent');
  
  return <div>...</div>;
}
*/
markdown
**📖 Documentation (README.md):**

# React Performance Monitor

Production-ready performance monitoring tool for React applications.

## Installation

```jsx
// 1. Wrap your app
import {
  PerformanceMonitorProvider,
  PerformanceMonitorDashboard,
} from './PerformanceMonitor';

function App() {
  return (
    <PerformanceMonitorProvider>
      <YourApp />
      <PerformanceMonitorDashboard />
    </PerformanceMonitorProvider>
  );
}

// 2. Track components
import { usePerformanceTracking } from './PerformanceMonitor';

function MyComponent() {
  usePerformanceTracking('MyComponent');
  // ...
}
```

## Features

- ✅ Real-time render tracking
- ✅ Performance warnings (slow renders, high counts)
- ✅ Keyboard shortcut (Ctrl+Shift+P)
- ✅ Export to JSON/CSV
- ✅ localStorage persistence
- ✅ Development-only (zero production overhead)

## Keyboard Shortcuts

- `Ctrl+Shift+P` - Toggle monitor on/off

## Thresholds

- High render count: > 10 renders
- Slow render: > 16ms

## API

### `usePerformanceTracking(componentName: string)`

Track renders for a component.

```jsx
function MyComponent() {
  usePerformanceTracking('MyComponent');
  return <div>...</div>;
}
```

### Export Data

Click "Export JSON" or "Export CSV" to download performance report.

```json
{
  "exportedAt": "2024-03-15T10:30:00.000Z",
  "metrics": {
    "MyComponent": {
      "renderCount": 15,
      "totalTime": 240.5,
      "avgTime": 16.03,
      "lastRenderTime": 18.2
    }
  },
  "warnings": [...]
}

```

## Performance Impact

- Development: < 1ms overhead per render
- Production: 0ms (completely disabled)

## Browser Support

- Chrome ✅
- Firefox ✅
- Safari ✅
- Edge ✅

## Limitations

- Development mode only
- Requires `performance.now()` API
- localStorage for persistence (optional)

📊 PHẦN 4: SO SÁNH PATTERNS (30 phút)

Bảng So Sánh: Tracking Render Behavior

ApproachProsConsWhen to Use
console.log trong component- Đơn giản nhất
- Không cần setup
- Nhiều noise
- Khó theo dõi
- Không có metrics
Quick debugging một component
useRef counter- Lightweight
- Không re-render
- Easy to implement
- Manual tracking
- Không có visualization
- Mỗi component riêng lẻ
Khi cần đếm renders của 1 component
React DevTools Profiler- Built-in React
- Visual flamegraph
- Timeline view
- No code changes
- Chỉ khi DevTools mở
- Không persist data
- Khó automate
Development analysis, one-time profiling
Custom Performance Monitor- Programmable
- Persist data
- Export reports
- Automation friendly
- Phức tạp implement
- Overhead nhỏ
- Maintenance burden
Production-ready apps, CI/CD integration

Decision Tree: Chọn Tracking Approach


START: Cần track render performance

├─ Quick debug 1 component?
│ └─ YES → console.log hoặc useRef counter

├─ Visual analysis 1 lần?
│ └─ YES → React DevTools Profiler

├─ Continuous monitoring?
│ └─ YES → Custom Performance Monitor

└─ Production monitoring?
└─ YES → Performance API + Analytics
(Bài học nâng cao - không trong scope ngày này)

So Sánh: State Updates & Re-renders

TriggerComponent Re-rendersChildren Re-renderDOM Updates
useState update✅ Always (unless bailout)✅ Always (default)⚠️ Only if Virtual DOM differs
Props change✅ Always✅ Always⚠️ Only if Virtual DOM differs
Parent rendersN/A✅ Always (default)⚠️ Only if Virtual DOM differs
Context change✅ All consumers✅ Children of consumers⚠️ Only if Virtual DOM differs
forceUpdate()✅ Always✅ Always⚠️ Only if Virtual DOM differs

💡 Key Insight:

  • Render ≠ DOM update
  • React is smart about DOM (only updates what changed)
  • Problem is JavaScript execution time, not DOM time!

🧪 PHẦN 5: DEBUG LAB (20 phút)

Bug 1: Infinite Render Loop 🔥

jsx
function BuggyCounter() {
  const [count, setCount] = useState(0);
  const renderCount = useRef(0);

  // 🐛 BUG: Tại sao component re-render vô hạn?
  renderCount.current += 1;
  console.log('Render #', renderCount.current);

  if (renderCount.current > 5) {
    setCount(count + 1);
  }

  return <div>Count: {count}</div>;
}

❓ Câu hỏi:

  1. Component này render bao nhiêu lần?
  2. Tại sao infinite loop?
  3. Fix như thế nào?
🔍 Debug Steps

Phân tích:

jsx
Initial render:
├─ renderCount.current = 1
├─ count = 0
└─ No setState

Render 2-5:
├─ renderCount.current = 2, 3, 4, 5
└─ No setState

Render 6:
├─ renderCount.current = 6
├─ Condition true (6 > 5)
├─ setCount(0 + 1) → count = 1
└─ State change → Re-render!

Render 7:
├─ renderCount.current = 7
├─ Condition true (7 > 5)
├─ setCount(1 + 1) → count = 2
└─ State change → Re-render!

Infinite loop! 🔥

Root Cause:

jsx
// setState trong render phase (component body) → FORBIDDEN!
if (renderCount.current > 5) {
  setCount(count + 1); // ❌ setState mỗi lần render!
}

✅ Solution:

jsx
// Option 1: Move to useEffect
useEffect(() => {
  if (renderCount.current > 5) {
    setCount(count + 1);
  }
}, []); // Chỉ chạy once!

// Option 2: Event handler
const handleClick = () => {
  if (renderCount.current > 5) {
    setCount(count + 1);
  }
};

// Option 3: Remove conditional setState
// (Không có lý do hợp lý để setState based on render count!)

💡 Rule:

NEVER call setState directly in component body! Only in:

  • Event handlers
  • useEffect
  • setTimeout/setInterval callbacks
  • Promise callbacks

Bug 2: Missing Re-render 🤔

jsx
function UserProfile() {
  const user = { name: 'John', age: 30 };

  const updateAge = () => {
    user.age = 31; // 🐛 Tại sao không re-render?
    console.log('User age updated:', user.age);
  };

  return (
    <div>
      <p>Age: {user.age}</p>
      <button onClick={updateAge}>Update Age</button>
    </div>
  );
}

❓ Câu hỏi:

  1. Click button, component có re-render không?
  2. Tại sao?
  3. Fix như thế nào?
🔍 Debug Steps

Phân tích:

jsx
Click button:
├─ updateAge() runs
├─ user.age = 31 (mutation)
├─ console.log shows 31 (value did change!)
├─ BUT: No state update
└─ React doesn't know to re-render! 

Why?
├─ user is a regular variable (not state)
├─ Mutation doesn't trigger re-render
└─ React only re-renders on:
    - setState
    - Props change
    - Context change
    - forceUpdate (không nên dùng)

Root Cause:

jsx
// Regular variable mutation ≠ State update
const user = { name: 'John', age: 30 }; // ❌ Not state!
user.age = 31; // ❌ Mutation, no trigger!

✅ Solution:

jsx
// Option 1: Use useState
function UserProfile() {
  const [user, setUser] = useState({ name: 'John', age: 30 });

  const updateAge = () => {
    // Immutable update
    setUser((prevUser) => ({
      ...prevUser,
      age: 31,
    }));
  };

  return (
    <div>
      <p>Age: {user.age}</p>
      <button onClick={updateAge}>Update Age</button>
    </div>
  );
}

// Option 2: Separate state for age
function UserProfile() {
  const [age, setAge] = useState(30);

  const updateAge = () => {
    setAge(31);
  };

  return (
    <div>
      <p>Age: {age}</p>
      <button onClick={updateAge}>Update Age</button>
    </div>
  );
}

💡 Rule:

React state must be treated as IMMUTABLE!

  • Never mutate objects/arrays directly
  • Always create new object/array
  • Use spread operator or methods that return new values

Bug 3: Unexpected Re-renders 😱

jsx
function ParentComponent() {
  const [parentCount, setParentCount] = useState(0);

  const childData = {
    name: 'Static Data',
    value: 100,
  };

  return (
    <div>
      <button onClick={() => setParentCount((c) => c + 1)}>
        Parent Count: {parentCount}
      </button>
      <ExpensiveChild data={childData} />
    </div>
  );
}

const ExpensiveChild = React.memo(({ data }) => {
  console.log('ExpensiveChild rendered'); // 🐛 Log mỗi lần parent render!

  // Expensive computation
  const result = Array(1000000)
    .fill(0)
    .reduce((sum, _, i) => sum + i, 0);

  return (
    <div>
      Child: {data.name} - {data.value}
      <br />
      Result: {result}
    </div>
  );
});

❓ Câu hỏi:

  1. ExpensiveChild có React.memo, tại sao vẫn re-render?
  2. Vấn đề ở đâu?
  3. Fix như thế nào?
🔍 Debug Steps

Phân tích:

jsx
Every Parent render:
├─ ParentComponent function runs
├─ childData = { name: '...', value: 100 }
│   └─ NEW object created! (different reference)
├─ <ExpensiveChild data={childData} />
│   └─ React.memo compares:
- prev data: { name: '...', value: 100 } @ 0x001
- new data:  { name: '...', value: 100 } @ 0x002
- 0x001 !== 0x002 → Different!
- Re-render child!
└─ ExpensiveChild renders (and does expensive computation!)

Why memo doesn't work?
├─ React.memo uses Object.is (shallow comparison)
├─ Objects compared by reference
├─ NEW object every render = different reference
└─ Memo thinks props changed!

Root Cause:

jsx
// Creating object in render
const childData = {
  /* ... */
}; // ❌ NEW object mỗi lần!

// React.memo comparison:
const prevData = { name: 'Static Data', value: 100 };
const nextData = { name: 'Static Data', value: 100 };
prevData === nextData; // false! (different references)

✅ Solution:

jsx
// Option 1: Move outside component (if truly static)
const STATIC_CHILD_DATA = {
  name: 'Static Data',
  value: 100,
};

function ParentComponent() {
  const [parentCount, setParentCount] = useState(0);

  return (
    <div>
      <button onClick={() => setParentCount((c) => c + 1)}>
        Parent Count: {parentCount}
      </button>
      <ExpensiveChild data={STATIC_CHILD_DATA} />
    </div>
  );
}

// Option 2: useMemo (ngày mai học!)
function ParentComponent() {
  const [parentCount, setParentCount] = useState(0);

  const childData = useMemo(
    () => ({
      name: 'Static Data',
      value: 100,
    }),
    [],
  ); // Stable reference

  return (
    <div>
      <button onClick={() => setParentCount((c) => c + 1)}>
        Parent Count: {parentCount}
      </button>
      <ExpensiveChild data={childData} />
    </div>
  );
}

// Option 3: useState (if actually state)
function ParentComponent() {
  const [parentCount, setParentCount] = useState(0);
  const [childData] = useState({
    name: 'Static Data',
    value: 100,
  }); // Initialized once, stable reference

  return (
    <div>
      <button onClick={() => setParentCount((c) => c + 1)}>
        Parent Count: {parentCount}
      </button>
      <ExpensiveChild data={childData} />
    </div>
  );
}

💡 Rule:

When using React.memo:

  • Props MUST have stable references
  • Objects/arrays created in render = new references
  • Use useMemo, useState, or define outside component
  • Tomorrow we learn useMemo properly!

✅ PHẦN 6: TỰ ĐÁNH GIÁ (15 phút)

Knowledge Check

Đánh dấu ✅ nếu bạn hiểu:

Render Basics:

  • [ ] Phân biệt Render phase vs Commit phase
  • [ ] Hiểu khi nào component re-render
  • [ ] Hiểu parent render → children render
  • [ ] Biết render ≠ DOM update

Props & State:

  • [ ] React so sánh props bằng Object.is
  • [ ] Primitives so sánh by value
  • [ ] Objects/arrays so sánh by reference
  • [ ] setState với giá trị giống nhau → bailout (sau lần đầu)

Debugging:

  • [ ] Sử dụng useRef để đếm renders
  • [ ] Sử dụng React DevTools Profiler
  • [ ] Highlight updates trong DevTools
  • [ ] Đọc Flamegraph view

Common Pitfalls:

  • [ ] Không setState trong component body
  • [ ] Không mutate state trực tiếp
  • [ ] Objects trong render = new reference
  • [ ] React.memo cần stable props

Code Review Checklist

Khi review code React, check:

Performance Red Flags:

  • [ ] ❌ Creating objects/arrays trong render
jsx
// Bad
const config = {
  /* ... */
}; // New object mỗi render!
<Child config={config} />;

// Good
const CONFIG = {
  /* ... */
}; // Outside component
<Child config={CONFIG} />;
  • [ ] ❌ setState trong component body
jsx
// Bad
if (condition) {
  setCount(count + 1); // Infinite loop potential!
}

// Good
useEffect(() => {
  if (condition) {
    setCount(count + 1);
  }
}, [condition, count]);
  • [ ] ❌ Inline functions làm props cho memoized children
jsx
// Bad
<MemoizedChild onClick={() => doSomething()} />;
// onClick is NEW function every render!

// Good (will learn tomorrow)
const handleClick = useCallback(() => doSomething(), []);
<MemoizedChild onClick={handleClick} />;

Đo lường trước khi optimize:

  • [ ] ✅ Profile với DevTools trước
  • [ ] ✅ Identify actual bottlenecks
  • [ ] ✅ Measure impact sau optimize
  • [ ] ✅ Don't optimize prematurely!

🏠 BÀI TẬP VỀ NHÀ

Bắt buộc (30 phút)

Exercise: Render Audit

  1. Lấy 1 app React cũ của bạn (hoặc create new nếu chưa có)
  2. Thêm render tracking vào 5-10 components
  3. Thực hiện thao tác thông thường (click, type, navigate)
  4. Ghi lại:
    • Component nào render nhiều nhất?
    • Có renders không cần thiết không?
    • Tại sao chúng render?
  5. Viết ngắn gọn (200-300 từ) phân tích findings

Deliverable: Document với:

  • Screenshots DevTools Profiler
  • Render count table
  • Top 3 optimization opportunities
💡 Solution
jsx
/**
 * @description
 * Bài tập về nhà - Render Audit
 * Ứng dụng đếm & hiển thị trạng thái render của nhiều component
 * Giúp dễ dàng nhận biết component nào render quá nhiều khi tương tác
 *
 * Chỉ sử dụng các hook đã học đến ngày 31:
 * • useState
 * • useRef
 * • useEffect
 */

import { useState, useRef, useEffect } from 'react';

// ────────────────────────────────────────────────
// Hook theo dõi số lần render + thời gian render gần nhất
function useRenderTracker(componentName) {
  const renderCount = useRef(0);
  const lastRenderTime = useRef(0);

  renderCount.current += 1;

  // Đo thời gian render (chỉ mang tính tương đối)
  const start = performance.now ? performance.now() : Date.now();

  useEffect(() => {
    const end = performance.now ? performance.now() : Date.now();
    lastRenderTime.current = end - start;
    console.log(
      `%c${componentName} rendered #${renderCount.current}  •  ${lastRenderTime.current.toFixed(2)}ms`,
      lastRenderTime.current > 8 ? 'color:#c41e3b' : 'color:#2e7d32',
    );
  });

  return { count: renderCount.current, ms: lastRenderTime.current };
}

// ────────────────────────────────────────────────
function RenderInfo({ name }) {
  const { count, ms } = useRenderTracker(name);

  const style = {
    background: count > 12 ? '#ffebee' : count > 6 ? '#fff3e0' : '#e8f5e9',
    padding: '4px 8px',
    borderRadius: '4px',
    fontSize: '11px',
    fontFamily: 'monospace',
    marginLeft: '10px',
  };

  return (
    <span style={style}>
      {name} #{count} {ms > 0 && `(${ms.toFixed(1)}ms)`}
    </span>
  );
}

// ────────────────────────────────────────────────
function CounterPanel() {
  const [a, setA] = useState(0);
  const [b, setB] = useState(0);

  return (
    <div
      style={{ border: '1px dashed #aaa', padding: '16px', margin: '16px 0' }}
    >
      <h3>
        Counter Panel <RenderInfo name='CounterPanel' />
      </h3>

      <div style={{ display: 'flex', gap: '24px', margin: '12px 0' }}>
        <div>
          <strong>A:</strong> {a}
          <button
            onClick={() => setA((c) => c + 1)}
            style={{ marginLeft: 8 }}
          >
            +1
          </button>
        </div>
        <div>
          <strong>B:</strong> {b}
          <button
            onClick={() => setB((c) => c + 1)}
            style={{ marginLeft: 8 }}
          >
            +1
          </button>
        </div>
      </div>

      <StaticChild />
      <DependentChild value={a} />
      <DependentChild value={b} />
    </div>
  );
}

// ────────────────────────────────────────────────
function StaticChild() {
  return (
    <div
      style={{
        margin: '12px 0',
        padding: '12px',
        background: '#f5f5f5',
        borderRadius: 6,
      }}
    >
      Static Child <RenderInfo name='StaticChild' />
      <p>Không nhận props → vẫn render mỗi khi parent render</p>
    </div>
  );
}

// ────────────────────────────────────────────────
function DependentChild({ value }) {
  return (
    <div
      style={{
        margin: '12px 0',
        padding: '12px',
        background: '#e3f2fd',
        borderRadius: 6,
      }}
    >
      Dependent Child (value = {value}) <RenderInfo name='DependentChild' />
      <p>Nhận props → render khi props thay đổi (hoặc parent render)</p>
    </div>
  );
}

// ────────────────────────────────────────────────
function SearchSimulator() {
  const [term, setTerm] = useState('');

  return (
    <div style={{ margin: '20px 0' }}>
      <h3>
        Search Simulator <RenderInfo name='SearchSimulator' />
      </h3>
      <input
        type='text'
        value={term}
        onChange={(e) => setTerm(e.target.value)}
        placeholder='Gõ để quan sát re-render...'
        style={{ width: '100%', padding: '10px', fontSize: '16px' }}
      />
      <p style={{ color: '#555', marginTop: 8, fontSize: '14px' }}>
        Mỗi ký tự gõ → toàn bộ app re-render (trừ khi đã memo)
      </p>
    </div>
  );
}

// ────────────────────────────────────────────────
export default function RenderAuditV2() {
  const [dark, setDark] = useState(false);

  return (
    <div
      style={{
        padding: '32px',
        maxWidth: 900,
        margin: '0 auto',
        background: dark ? '#121212' : '#fff',
        color: dark ? '#e0e0e0' : '#000',
        minHeight: '100vh',
        transition: 'all 0.3s',
      }}
    >
      <div
        style={{
          display: 'flex',
          justifyContent: 'space-between',
          alignItems: 'center',
        }}
      >
        <h1>Render Audit Lab v2</h1>
        <button
          onClick={() => setDark(!dark)}
          style={{ padding: '8px 16px' }}
        >
          {dark ? 'Light' : 'Dark'}
        </button>
      </div>

      <p style={{ color: dark ? '#aaa' : '#555', marginBottom: '32px' }}>
        Quan sát console + badge để xem: • Component nào render khi nào • Số lần
        render & thời gian tương đối • Những render không cần thiết (màu đỏ /
        cam)
      </p>

      <SearchSimulator />

      <CounterPanel />
      <CounterPanel />

      <div
        style={{
          marginTop: 40,
          padding: 16,
          background: dark ? '#1e1e1e' : '#f8f9fa',
          borderRadius: 8,
        }}
      >
        <h3>Kết luận nhanh từ bài tập này:</h3>
        <ul>
          <li>
            Mỗi lần state thay đổi ở component cha →{' '}
            <strong>tất cả con cháu render lại</strong>
          </li>
          <li>Component không nhận props vẫn render → lãng phí</li>
          <li>
            Component nhận props nhưng props không đổi vẫn render → lãng phí
          </li>
          <li>
            Mỗi lần gõ search → toàn bộ cây component render → rất dễ gây lag
            khi cây lớn
          </li>
        </ul>
        <p style={{ marginTop: 16, fontWeight: 'bold' }}>
          → Ngày mai (React.memo) sẽ giúp giải quyết hầu hết các vấn đề trên
        </p>
      </div>
    </div>
  );
}

Kết quả quan sát điển hình khi tương tác:

Gõ 1 ký tự vào SearchSimulator:
→ RenderAuditV2 #N
→ SearchSimulator #N+1
→ CounterPanel #N+1
→ StaticChild #N+1
→ DependentChild #N+1 (value=0)
→ DependentChild #N+1 (value=0)
→ CounterPanel #N+2 (component thứ hai)
→ StaticChild #N+2
→ DependentChild #N+2 (value=0)
→ DependentChild #N+2 (value=0)

Nhấn +1 của A ở CounterPanel đầu tiên:
→ RenderAuditV2 #M
→ CounterPanel #M+1 (chỉ panel đầu)
→ StaticChild #M+1
→ DependentChild #M+1 (value = giá trị mới)
→ DependentChild #M+1 (value = 0)
→ CounterPanel #M+2 (panel thứ hai vẫn render!)
→ StaticChild #M+2
→ DependentChild #M+2 (value=0)
→ DependentChild #M+2 (value=0)

→ Rõ ràng thấy lãng phí rất lớn khi chưa có memoization

Nâng cao (60 phút)

Exercise: Performance Comparison

Implement cùng 1 feature theo 2 cách:

Feature: Todo list với search filter

Version A: Không optimize

jsx
function TodoApp() {
  const [todos, setTodos] = useState([
    /* 50 todos */
  ]);
  const [searchTerm, setSearchTerm] = useState('');

  const filtered = todos.filter((todo) =>
    todo.text.toLowerCase().includes(searchTerm.toLowerCase()),
  );

  return (
    <div>
      <input
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
      />
      {filtered.map((todo) => (
        <TodoItem
          key={todo.id}
          todo={todo}
        />
      ))}
    </div>
  );
}

Version B: Đã chuẩn bị cho optimize (ngày mai)

jsx
// Tổ chức code sao cho:
// - Dễ thêm React.memo
// - Dễ thêm useMemo
// - Dễ thêm useCallback
// (Chưa dùng những thứ này, chỉ structure!)

So sánh:

  1. Render counts (use tracking)
  2. DevTools Profiler results
  3. User experience (subjective)
  4. Sẵn sàng cho optimization ngày mai
💡 Solution
jsx
/**
 * @description
 * Bài tập nâng cao - Performance Comparison
 * So sánh 2 phiên bản Todo App:
 *   Version A: Không optimize (viết theo cách tự nhiên)
 *   Version B: Đã cấu trúc sẵn sàng cho optimization (dễ thêm memo/useMemo/useCallback sau này)
 *
 * Mục tiêu:
 * - Đo render count của từng phần
 * - Quan sát sự khác biệt khi gõ search và thêm todo
 * - Chuẩn bị codebase cho ngày mai (React.memo + useMemo + useCallback)
 *
 * Chỉ dùng hook đã học đến ngày 31:
 * - useState
 * - useRef
 * - useEffect
 */

import { useState, useRef, useEffect } from 'react';

// ────────────────────────────────────────────────
// Hook đếm render + log có màu
function useRenderCountTracker(name) {
  const count = useRef(0);
  count.current += 1;

  useEffect(() => {
    console.log(
      `%c[${name}] rendered → #${count.current}`,
      count.current > 10
        ? 'background:#ffebee;color:#c62828;padding:2px 6px;border-radius:3px'
        : 'background:#e8f5e9;color:#2e7d32;padding:2px 6px;border-radius:3px',
    );
  });

  return count.current;
}

// ────────────────────────────────────────────────
// Badge hiển thị số render (UI feedback)
function RenderBadge({ name }) {
  const renders = useRenderCountTracker(name);
  return (
    <sup
      style={{
        background:
          renders > 12 ? '#ef5350' : renders > 6 ? '#ffb74d' : '#81c784',
        color: 'white',
        padding: '2px 6px',
        borderRadius: '10px',
        fontSize: '10px',
        marginLeft: '6px',
      }}
    >
      #{renders}
    </sup>
  );
}

// ────────────────────────────────────────────────
// Todo Item chung cho cả 2 version
function TodoItem({ todo }) {
  useRenderCountTracker(`TodoItem-${todo.id}`);
  return (
    <li style={{ padding: '8px 0', borderBottom: '1px solid #eee' }}>
      {todo.text} {todo.completed && '✓'}
      <RenderBadge name={`Todo-${todo.id}`} />
    </li>
  );
}

// ────────────────────────────────────────────────
// ── VERSION A: Không optimize ────────────────────
// Viết theo kiểu thông thường → nhiều render thừa
function TodoAppVersionA({ initialTodos }) {
  const [todos, setTodos] = useState(initialTodos);
  const [search, setSearch] = useState('');

  // Filter ngay trong render → tạo mảng mới mỗi lần
  const visibleTodos = todos.filter((t) =>
    t.text.toLowerCase().includes(search.toLowerCase()),
  );

  const addTodo = (text) => {
    setTodos((prev) => [...prev, { id: Date.now(), text, completed: false }]);
  };

  useRenderCountTracker('VersionA-App');

  return (
    <div
      style={{
        border: '2px solid #ef5350',
        padding: '16px',
        borderRadius: 8,
        marginBottom: 32,
      }}
    >
      <h3>
        Version A - Không optimize <RenderBadge name='VersionA-App' />
      </h3>

      <AddTodoInput onAdd={addTodo} />
      <SearchInput
        value={search}
        onChange={setSearch}
      />

      <p style={{ color: '#555', margin: '12px 0' }}>
        Hiển thị: {visibleTodos.length} / {todos.length}
      </p>

      <ul style={{ listStyle: 'none', padding: 0 }}>
        {visibleTodos.map((todo) => (
          <TodoItem
            key={todo.id}
            todo={todo}
          />
        ))}
      </ul>
    </div>
  );
}

function AddTodoInput({ onAdd }) {
  const [text, setText] = useState('');
  useRenderCountTracker('VersionA-AddInput');

  const handleSubmit = (e) => {
    e.preventDefault();
    if (!text.trim()) return;
    onAdd(text.trim());
    setText('');
  };

  return (
    <form
      onSubmit={handleSubmit}
      style={{ marginBottom: 16 }}
    >
      <input
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder='Thêm todo...'
        style={{ width: '70%', padding: 8 }}
      />
      <button
        type='submit'
        style={{ padding: '8px 16px' }}
      >
        Thêm
      </button>
      <RenderBadge name='VersionA-AddInput' />
    </form>
  );
}

function SearchInput({ value, onChange }) {
  useRenderCountTracker('VersionA-Search');
  return (
    <div>
      <input
        value={value}
        onChange={(e) => onChange(e.target.value)}
        placeholder='Tìm kiếm...'
        style={{ width: '100%', padding: 8, marginBottom: 12 }}
      />
      <RenderBadge name='VersionA-Search' />
    </div>
  );
}

// ────────────────────────────────────────────────
// ── VERSION B: Chuẩn bị sẵn sàng optimize ────────
// Tách logic, đặt tên props rõ ràng, dễ memo sau này
function TodoAppVersionB({ initialTodos }) {
  const [todos, setTodos] = useState(initialTodos);
  const [searchTerm, setSearchTerm] = useState('');

  const addTodo = (text) => {
    setTodos((prev) => [...prev, { id: Date.now(), text, completed: false }]);
  };

  useRenderCountTracker('VersionB-App');

  return (
    <div
      style={{ border: '2px solid #1976d2', padding: '16px', borderRadius: 8 }}
    >
      <h3>
        Version B - Sẵn sàng optimize <RenderBadge name='VersionB-App' />
      </h3>

      <AddTodoSection onAdd={addTodo} />
      <SearchSection
        searchTerm={searchTerm}
        onSearchChange={setSearchTerm}
      />

      <TodoListSection
        todos={todos}
        searchTerm={searchTerm}
      />

      <div style={{ marginTop: 16, color: '#555', fontSize: '14px' }}>
        <strong>Lưu ý cấu trúc:</strong>
        <br />
        • Tách thành section rõ ràng
        <br />
        • Truyền callback ổn định (dù chưa useCallback)
        <br />• Dễ thêm React.memo / useMemo / useCallback ngày mai
      </div>
    </div>
  );
}

function AddTodoSection({ onAdd }) {
  const [text, setText] = useState('');
  useRenderCountTracker('VersionB-AddSection');

  const handleAdd = (e) => {
    e.preventDefault();
    if (!text.trim()) return;
    onAdd(text.trim());
    setText('');
  };

  return (
    <form
      onSubmit={handleAdd}
      style={{ marginBottom: 16 }}
    >
      <input
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder='Thêm todo mới...'
        style={{ width: '70%', padding: 8 }}
      />
      <button
        type='submit'
        style={{ padding: '8px 16px' }}
      >
        Thêm
      </button>
      <RenderBadge name='VersionB-Add' />
    </form>
  );
}

function SearchSection({ searchTerm, onSearchChange }) {
  useRenderCountTracker('VersionB-SearchSection');
  return (
    <div style={{ marginBottom: 16 }}>
      <input
        value={searchTerm}
        onChange={(e) => onSearchChange(e.target.value)}
        placeholder='Tìm kiếm todo...'
        style={{ width: '100%', padding: 8 }}
      />
      <RenderBadge name='VersionB-Search' />
    </div>
  );
}

function TodoListSection({ todos, searchTerm }) {
  useRenderCountTracker('VersionB-ListSection');

  // Vẫn filter trong render (sẽ move vào useMemo ngày mai)
  const filtered = todos.filter((t) =>
    t.text.toLowerCase().includes(searchTerm.toLowerCase()),
  );

  return (
    <div>
      <p style={{ color: '#555' }}>
        Kết quả: {filtered.length} / {todos.length}
      </p>
      <ul style={{ listStyle: 'none', padding: 0 }}>
        {filtered.map((todo) => (
          <TodoItem
            key={todo.id}
            todo={todo}
          />
        ))}
      </ul>
      <RenderBadge name='VersionB-List' />
    </div>
  );
}

// ────────────────────────────────────────────────
// App chính - chạy song song 2 version
export default function PerformanceComparison() {
  const initialTodos = [
    { id: 1, text: 'Học React rendering', completed: true },
    { id: 2, text: 'Hiểu useEffect cleanup', completed: true },
    { id: 3, text: 'Master useReducer patterns', completed: false },
    { id: 4, text: 'Tối ưu performance với memo', completed: false },
    { id: 5, text: 'Sử dụng React DevTools Profiler', completed: false },
    { id: 6, text: 'Tránh re-render không cần thiết', completed: false },
    { id: 7, text: 'Hiểu Object.is và shallow compare', completed: false },
  ];

  return (
    <div style={{ padding: '32px', maxWidth: 1000, margin: '0 auto' }}>
      <h1 style={{ textAlign: 'center', marginBottom: 40 }}>
        Performance Comparison - Todo App
      </h1>

      <TodoAppVersionA initialTodos={initialTodos} />
      <TodoAppVersionB initialTodos={initialTodos} />

      <div
        style={{
          marginTop: 48,
          padding: 20,
          background: '#f5f5f5',
          borderRadius: 8,
        }}
      >
        <h3>Kết luận sau khi test:</h3>
        <ul style={{ lineHeight: 1.6 }}>
          <li>
            Cả hai version hiện tại có số render gần giống nhau (vì chưa memo)
          </li>
          <li>Version A: code ngắn gọn nhưng khó mở rộng optimize</li>
          <li>
            Version B: dài hơn nhưng đã tách biệt rõ ràng:
            <ul>
              <li>
                Dễ bọc React.memo quanh AddTodoSection, SearchSection, TodoItem
              </li>
              <li>Dễ bọc useMemo quanh filtered todos</li>
              <li>Dễ bọc useCallback quanh onAdd, onSearchChange</li>
            </ul>
          </li>
          <li>Sau khi học ngày 32-34 → Version B sẽ giảm 70-90% render thừa</li>
        </ul>
      </div>
    </div>
  );
}

Kết quả quan sát điển hình (trước khi optimize):

Gõ 5 ký tự vào search (ví dụ: "học react"):

Version A:
→ VersionA-App × 5
→ VersionA-AddInput × 5
→ VersionA-Search × 5
→ VersionA-List (ẩn trong TodoAppVersionA) × 5
→ TodoItem-1 → TodoItem-7 × 5 lần mỗi item (tổng ~35 TodoItem renders)

Version B:
→ VersionB-App × 5
→ VersionB-AddSection × 5
→ VersionB-SearchSection × 5
→ VersionB-ListSection × 5
→ TodoItem-1 → TodoItem-7 × 5 lần mỗi item (tương tự Version A)

→ Hiện tại chưa khác biệt lớn về render count
→ Nhưng Version B đã sẵn sàng để giảm mạnh render sau khi thêm:
  • React.memo(TodoItem)
  • useMemo cho filtered list
  • useCallback cho các handler

→ Ngày mai sẽ thấy Version B vượt trội rõ rệt sau khi áp dụng các kỹ thuật mới

📚 TÀI LIỆU THAM KHẢO

Bắt buộc đọc

  1. React Docs - Render and Commit

  2. React Docs - Preserving and Resetting State

Đọc thêm

  1. A (Mostly) Complete Guide to React Rendering Behavior by Mark Erikson

  2. Before You memo() by Dan Abramov

  3. React DevTools Profiler Tutorial


🔗 KẾT NỐI KIẾN THỨC

Kiến thức nền (đã học)

Ngày 11-14: useState

  • State changes trigger re-renders
  • Functional updates để tránh stale closure

Ngày 16-20: useEffect

  • Chạy AFTER render (Commit phase)
  • Dependencies ảnh hưởng re-render behavior

Ngày 21-22: useRef

  • Persist values WITHOUT re-render
  • Perfect cho render tracking!

Ngày 26-29: useReducer

  • Complex state cũng trigger re-renders
  • Same render rules như useState

Hướng tới (sẽ học)

Ngày 32: React.memo

  • Prevent unnecessary child re-renders
  • Shallow props comparison

Ngày 33: useMemo

  • Memoize expensive computations
  • Stable object/array references

Ngày 34: useCallback

  • Memoize functions
  • Stable callback references

Ngày 35: Integration Project

  • Apply tất cả optimization techniques
  • Real-world performance tuning

💡 SENIOR INSIGHTS

Cân Nhắc Production

1. Premature Optimization is the Root of All Evil

jsx
// ❌ KHÔNG NÊN: Optimize ngay từ đầu
const MyComponent = React.memo(() => {
  const value = useMemo(() => computeSomething(), []);
  const callback = useCallback(() => doSomething(), []);
  // ... more memoization ...
});

// ✅ NÊN: Viết code đơn giản trước
const MyComponent = () => {
  const value = computeSomething();
  const callback = () => doSomething();
  // ...
};

// Chỉ optimize KHI:
// 1. Đo lường thấy vấn đề
// 2. Profiler chỉ ra bottleneck
// 3. User experience bị ảnh hưởng

Tại sao?

  • Optimization code phức tạp hơn
  • Harder to maintain
  • Performance gain có thể không đáng kể
  • Có thể introduce bugs

2. Not All Re-renders Are Bad

jsx
// Component này render mỗi giây, có sao không?
function Clock() {
  const [time, setTime] = useState(new Date());

  useEffect(() => {
    const id = setInterval(() => setTime(new Date()), 1000);
    return () => clearInterval(id);
  }, []);

  return <div>{time.toLocaleTimeString()}</div>;
}

// Absolutely fine!
// - Render nhanh (< 1ms)
// - User expects update mỗi giây
// - No complex children
// → KHÔNG CẦN OPTIMIZE!

Rules of thumb:

  • Render < 16ms → Probably OK
  • No user complaints → Don't optimize
  • Simple components → Re-render is cheap

3. Context is King

Performance optimization phụ thuộc context:

jsx
// E-commerce product page:
// - ProductImage: Optimize! (large images, expensive)
// - ProductPrice: Maybe (updates frequently)
// - ProductReviews: Definitely! (100+ items)
// - AddToCartButton: Probably not (simple, cheap)

// Real-time chat app:
// - MessageList: Optimize! (hundreds of messages)
// - ChatInput: Don't optimize (user typing, expects updates)
// - TypingIndicator: Don't optimize (animates frequently)

4. Mobile Matters More

jsx
// Desktop: 60 FPS, powerful CPU
// → Can tolerate more renders

// Mobile: Battery, slower CPU, thermal throttling
// → Be more aggressive with optimization

// Testing strategy:
// 1. Profile on low-end mobile (old iPhone/Android)
// 2. Use DevTools CPU throttling (6x slowdown)
// 3. Test on 3G network simulation

Câu Hỏi Phỏng Vấn

Junior Level:

Q1: "Khi nào React component re-render?"

Expected answer:

  • Khi state thay đổi (useState, useReducer)
  • Khi props thay đổi
  • Khi parent component re-render
  • Khi Context value thay đổi (sẽ học sau)

Q2: "Render phase và Commit phase khác nhau thế nào?"

Expected answer:

  • Render: Gọi component function, tạo Virtual DOM, reconciliation
  • Commit: Update Real DOM, run useLayoutEffect, browser paint, run useEffect
  • Render có thể interrupt, Commit không thể

Mid Level:

Q3: "React so sánh props như thế nào?"

Expected answer:

  • Sử dụng Object.is (tương tự ===)
  • Shallow comparison (chỉ compare references, không deep equal)
  • Primitives: Compare by value
  • Objects/arrays: Compare by reference
  • Example: { name: 'John' } !== { name: 'John' } (different references)

Q4: "Làm sao debug performance issues trong React app?"

Expected answer:

  1. React DevTools Profiler
  2. useRef để track render counts
  3. console.log (có thể dùng highlight updates)
  4. Performance API (performance.now())
  5. Identify unnecessary renders
  6. Check expensive computations
  7. Verify stable object/array references

Senior Level:

Q5: "Explain React's bailout optimization. Khi nào nó work, khi nào không?"

Expected answer:

  • React bails out (skips re-render) khi setState với cùng value
  • Object.is comparison: setState(5) khi state = 5 → bailout
  • NHƯNG: Chỉ sau lần check đầu tiên
  • Example:
    jsx
    setCount(0); // First time: Re-renders (React checks after)
    setCount(0); // Second time: Bailout! (React: "0 === 0, skip")
  • Doesn't work với new object: setUser({ name: 'John' })
  • Objects compared by reference, not content

Q6: "Khi nào bạn quyết định optimize React component? Walk me through your process."

Expected answer:

  1. Measure First

    • React DevTools Profiler
    • Identify slow renders (> 16ms)
    • Find unnecessary renders
  2. Prioritize

    • Components with many children
    • Expensive computations
    • High render frequency
  3. Choose Technique

    • React.memo for components
    • useMemo for values
    • useCallback for functions
    • Split components
    • Lazy loading
  4. Verify

    • Profile again
    • Measure improvement
    • Check trade-offs (code complexity, memory)
  5. Document

    • Why optimized
    • Expected benefit
    • Maintenance notes

War Stories

Story 1: The 10,000 Item List 🔥

Scenario:
- Dashboard với table 10,000 rows
- Mỗi lần search → Re-render tất cả rows
- App freeze 2-3 giây!

Root Cause:
- ProductRow component không memo
- Table re-render → 10,000 ProductRow re-render
- Mỗi row có expensive computation (price calculations)

Solution:
- React.memo on ProductRow
- useMemo for price calculation
- Virtualization (react-window) cho visible rows only

Result:
- 2-3 seconds → < 100ms
- Smooth 60 FPS scrolling
- Happy users, happy PM!

Lesson:
- Số lượng quan trọng hơn độ phức tạp
- 10,000 simple components > 10 complex components
- Consider virtualization for long lists

Story 2: The Mysterious Lag 🤔

Scenario:
- Simple form input lag khi typing
- DevTools shows parent re-rendering 100+ components
- BUT: Form input không có children!

Root Cause:
- Input trong top-level App component
- App state change → Entire app re-renders
- Tất cả routes, modals, sidebars re-render!

Solution:
- Move input state to separate component
- Prevent state hoisting quá cao
- Use Context (học sau) cho shared state

Result:
- Typing lag gone
- App feels snappy
- Code actually simpler!

Lesson:
- State càng cao → Re-render càng rộng
- Keep state as low as possible
- "Lift state up" có giới hạn
- Sometimes need Context/Redux

Story 3: Over-Optimization Backfire 😅

Scenario:
- Junior dev learn về React.memo
- Wrap EVERYTHING với memo, useMemo, useCallback
- Code become nightmare to maintain
- NO performance improvement!

Problem:
- Simple components (< 1ms render)
- Memoization overhead > render cost
- Dependencies change often → Memo useless
- Code phức tạp 3x, no benefit

Solution:
- REMOVE most memoization
- Keep only where measured improvement
- Focus on actual bottlenecks
- Profile before & after

Result:
- Code simpler
- Same (or better!) performance
- Team happier

Lesson:
- "If in doubt, leave it out"
- Measure first, optimize second
- Simple code > premature optimization
- Trust React's default behavior

🎯 TÓM TẮT NGÀY 31

Những điều quan trọng nhất:

  1. React Render Cycle:

    • Render Phase: Call functions, create Virtual DOM, reconcile
    • Commit Phase: Update DOM, run effects
    • Render ≠ DOM update!
  2. Re-render Triggers:

    • State change (useState, useReducer)
    • Props change
    • Parent renders → Children render (default)
    • Context change (sẽ học)
  3. Props Comparison:

    • Object.is (shallow, reference-based)
    • Primitives: By value
    • Objects/arrays: By reference
    • NEW object !== NEW object (even same content)
  4. Debugging Tools:

    • React DevTools Profiler (visual)
    • useRef counter (programmatic)
    • console.log (quick)
    • Custom monitoring (production)
  5. Common Pitfalls:

    • Creating objects/arrays in render
    • setState in component body
    • Mutating state directly
    • Premature optimization

Chuẩn bị cho ngày mai:

Ngày 32 sẽ học React.memo - công cụ đầu tiên để optimize renders!

Preview:

jsx
// Today: All children re-render
function Parent() {
  const [count, setCount] = useState(0);
  return (
    <>
      <button onClick={() => setCount((c) => c + 1)}>{count}</button>
      <Child /> {/* Re-renders when count changes! */}
    </>
  );
}

// Tomorrow: Prevent unnecessary re-renders
const Child = React.memo(() => {
  return <div>I only render when MY props change!</div>;
});

Homework trước Ngày 32:

  • Ôn lại props comparison (Object.is)
  • Review stable references concept
  • Làm bài tập về nhà (render audit)
  • Suy nghĩ: Components nào trong project của bạn render quá nhiều?

🎉 Congratulations! Bạn đã hoàn thành Ngày 31 - nền tảng cho Performance Optimization! Ngày mai sẽ học cách FIX những vấn đề mình vừa discover hôm nay.

💪 Keep going! Performance optimization là skill quan trọng nhất để từ Mid lên Senior React Developer!

Personal tech knowledge base