📅 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?
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<Child1 count={count} />
<Child2 />
</div>
);
}💡 Đáp án
Cả 3 components đều re-render!
Parentre-render vì state thay đổiChild1re-render vì parent renderChild2re-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!
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ự →
newTodostate thay đổi TodoAppre-render- TẤT CẢ 100
TodoItemcomponents 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:
- Hiểu CHI TIẾT cách React render
- ĐO LƯỜNG performance
- NHẬN BIẾT vấn đề
- 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:
// 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:
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:
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
// 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:
- Click "Parent Count" button
- Quan sát console
- 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
// 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:
// 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 propDemo 3: State Updates & Bailout Optimization ⭐⭐⭐
Mục tiêu: React's bailout optimization khi setState giá trị giống nhau
// 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:
// ✅ 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:
Cài đặt: React DevTools extension (Chrome/Firefox)
Mở Profiler tab:
- F12 → React DevTools → Profiler
- Click "Record" button (⏺️)
Thực hiện actions:
- Click buttons, type, etc.
- Stop recording (⏹️)
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:
// 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)
/**
* 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
// 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
/**
* 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:
// 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 đổiOptimization Plan (sẽ implement ngày mai):
// 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
/**
* 📋 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
// 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:
- Context for Global State:
// Tất cả components share same render tracking state
// Không cần props drilling- useEffect without deps:
useEffect(() => {
// Runs on EVERY render - perfect for tracking!
trackRender(componentName);
});- Sorting & Highlighting:
// 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
/**
* 🏗️ 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 × 10Render 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):
# 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
// 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 performancePros:
- Simplest code
- No maintenance burden Cons:
- Poor UX
- User complaints
- Competitive disadvantage
Decision: Rejected. Must optimize.
## 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
/**
* 📋 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
// 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>;
}
*/**📖 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
| Approach | Pros | Cons | When 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
| Trigger | Component Re-renders | Children Re-render | DOM Updates |
|---|---|---|---|
| useState update | ✅ Always (unless bailout) | ✅ Always (default) | ⚠️ Only if Virtual DOM differs |
| Props change | ✅ Always | ✅ Always | ⚠️ Only if Virtual DOM differs |
| Parent renders | N/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 🔥
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:
- Component này render bao nhiêu lần?
- Tại sao infinite loop?
- Fix như thế nào?
🔍 Debug Steps
Phân tích:
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:
// setState trong render phase (component body) → FORBIDDEN!
if (renderCount.current > 5) {
setCount(count + 1); // ❌ setState mỗi lần render!
}✅ Solution:
// 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 🤔
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:
- Click button, component có re-render không?
- Tại sao?
- Fix như thế nào?
🔍 Debug Steps
Phân tích:
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:
// Regular variable mutation ≠ State update
const user = { name: 'John', age: 30 }; // ❌ Not state!
user.age = 31; // ❌ Mutation, no trigger!✅ Solution:
// 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 😱
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:
- ExpensiveChild có React.memo, tại sao vẫn re-render?
- Vấn đề ở đâu?
- Fix như thế nào?
🔍 Debug Steps
Phân tích:
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:
// 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:
// 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
// Bad
const config = {
/* ... */
}; // New object mỗi render!
<Child config={config} />;
// Good
const CONFIG = {
/* ... */
}; // Outside component
<Child config={CONFIG} />;- [ ] ❌ setState trong component body
// 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
// 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
- Lấy 1 app React cũ của bạn (hoặc create new nếu chưa có)
- Thêm render tracking vào 5-10 components
- Thực hiện thao tác thông thường (click, type, navigate)
- 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?
- 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
/**
* @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ó memoizationNâ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
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)
// 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:
- Render counts (use tracking)
- DevTools Profiler results
- User experience (subjective)
- Sẵn sàng cho optimization ngày mai
💡 Solution
/**
* @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
React Docs - Render and Commit
- https://react.dev/learn/render-and-commit
- Đọc 2-3 lần cho thật hiểu!
React Docs - Preserving and Resetting State
- https://react.dev/learn/preserving-and-resetting-state
- Hiểu khi nào state giữ lại, khi nào reset
Đọc thêm
A (Mostly) Complete Guide to React Rendering Behavior by Mark Erikson
Before You memo() by Dan Abramov
- https://overreacted.io/before-you-memo/
- Quan điểm cân bằng về optimization
React DevTools Profiler Tutorial
- https://react.dev/learn/react-developer-tools
- Official guide
🔗 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
// ❌ 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ưởngTạ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
// 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:
// 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
// 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 simulationCâ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:
- React DevTools Profiler
- useRef để track render counts
- console.log (có thể dùng highlight updates)
- Performance API (performance.now())
- Identify unnecessary renders
- Check expensive computations
- 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:
Measure First
- React DevTools Profiler
- Identify slow renders (> 16ms)
- Find unnecessary renders
Prioritize
- Components with many children
- Expensive computations
- High render frequency
Choose Technique
- React.memo for components
- useMemo for values
- useCallback for functions
- Split components
- Lazy loading
Verify
- Profile again
- Measure improvement
- Check trade-offs (code complexity, memory)
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 listsStory 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/ReduxStory 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:
React Render Cycle:
- Render Phase: Call functions, create Virtual DOM, reconcile
- Commit Phase: Update DOM, run effects
- Render ≠ DOM update!
Re-render Triggers:
- State change (useState, useReducer)
- Props change
- Parent renders → Children render (default)
- Context change (sẽ học)
Props Comparison:
- Object.is (shallow, reference-based)
- Primitives: By value
- Objects/arrays: By reference
- NEW object !== NEW object (even same content)
Debugging Tools:
- React DevTools Profiler (visual)
- useRef counter (programmatic)
- console.log (quick)
- Custom monitoring (production)
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:
// 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!