📅 NGÀY 46: Concurrent Rendering - Nền Tảng Hiện Đại
🎯 Mục tiêu học tập (5 phút)
- [ ] Hiểu được Concurrent Rendering là gì và tại sao cần thiết
- [ ] Phân biệt rõ Synchronous vs Concurrent rendering
- [ ] Nắm vững Automatic Batching trong React 18
- [ ] Biết cách measure và visualize render performance
- [ ] Nhận diện use cases phù hợp với Concurrent features
🤔 Kiểm tra đầu vào (5 phút)
Câu 1: State update trong React trigger re-render như thế nào? Có đồng bộ hay bất đồng bộ?
Câu 2: Khi bạn gọi setState nhiều lần liên tiếp, React xử lý như thế nào?
Câu 3: Trong app có danh sách 10,000 items, user typing vào search box bị lag. Tại sao và bạn xử lý thế nào?
📖 PHẦN 1: GIỚI THIỆU KHÁI NIỆM (30 phút)
1.1 Vấn Đề Thực Tế
Tưởng tượng bạn đang build một search interface:
function SearchPage() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const handleChange = (e) => {
const value = e.target.value;
setQuery(value);
// Heavy computation - filter 10,000 items
const filtered = heavyFilter(allItems, value);
setResults(filtered);
};
return (
<>
<input
value={query}
onChange={handleChange}
/>
<ResultsList results={results} /> {/* Renders 10,000 items */}
</>
);
}Vấn đề: Mỗi keystroke → UI freezes vài trăm ms → Trải nghiệm tệ!
Tại sao? React 17 render đồng bộ (synchronous):
- User types "a"
- React bắt đầu render từ đầu đến cuối
- PHẢI hoàn thành 100% trước khi handle input tiếp theo
- Nếu render takes 200ms → Input bị block 200ms
1.2 Giải Pháp: Concurrent Rendering
React 18 giới thiệu Concurrent Rendering - khả năng:
- Interruptible rendering: Tạm dừng render để xử lý urgent updates
- Prioritized updates: Updates có độ ưu tiên khác nhau
- Automatic batching: Batch nhiều state updates thành 1 render
Mental Model:
REACT 17 (Synchronous):
User Input → [==========RENDER==========] → UI Update
(blocked, không thể interrupt)
REACT 18 (Concurrent):
User Input → [==RENDER==] ← New Input!
(pause render, handle input first)
→ [==RENDER URGENT==] → UI Update (fast!)
→ [==RESUME RENDER==] → Background Update1.3 Mental Model
Hãy nghĩ về Concurrent Rendering như multitasking trên computer:
Synchronous (React 17):
Task A [=============================] Task B
Must finish A before start BConcurrent (React 18):
Task A [====] [====] [====]
Task B [====] [====] [====]
Switch between tasks, prioritize urgent onesAnalogy:
- Synchronous: Như đọc sách từ đầu đến cuối, không được dừng
- Concurrent: Như đọc sách nhưng có thể bookmark, xử lý điện thoại, rồi quay lại đọc tiếp
1.4 Hiểu Lầm Phổ Biến
❌ Hiểu lầm 1: "Concurrent = Parallel (đa luồng)"
- ✅ Sự thật: Vẫn single-threaded! Chỉ là time-slicing thông minh
❌ Hiểu lầm 2: "React 18 tự động làm mọi thứ nhanh hơn"
- ✅ Sự thật: Cần opt-in vào concurrent features (useTransition, useDeferredValue)
❌ Hiểu lầm 3: "Phải rewrite toàn bộ app để dùng React 18"
- ✅ Sự thật: Backward compatible 100%! Chỉ cần upgrade version
❌ Hiểu lầm 4: "Automatic batching làm app chậm hơn"
- ✅ Sự thật: Ngược lại - ít render hơn = nhanh hơn!
💻 PHẦN 2: LIVE CODING (45 phút)
Demo 1: Automatic Batching - Before vs After ⭐
React 17 (No Batching ngoài event handlers):
/**
* Demo: React 17 batching behavior
* Batching CHỈ hoạt động trong event handlers
*/
function Counter17() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
console.log('Render'); // Log để đếm số lần render
// ✅ Event handler: Batched (1 render)
const handleClick = () => {
setCount((c) => c + 1);
setFlag((f) => !f);
// React 17: Batches these → 1 render
};
// ❌ setTimeout: NOT batched (2 renders)
const handleAsync = () => {
setTimeout(() => {
setCount((c) => c + 1); // Render 1
setFlag((f) => !f); // Render 2
// React 17: NO batching → 2 renders!
}, 1000);
};
// ❌ fetch: NOT batched (2 renders)
const handleFetch = () => {
fetch('/api/data').then(() => {
setCount((c) => c + 1); // Render 1
setFlag((f) => !f); // Render 2
// React 17: NO batching → 2 renders!
});
};
return (
<div>
<p>
Count: {count}, Flag: {flag.toString()}
</p>
<button onClick={handleClick}>Sync Update (1 render)</button>
<button onClick={handleAsync}>Async Update (2 renders)</button>
<button onClick={handleFetch}>Fetch Update (2 renders)</button>
</div>
);
}
// Console output khi click "Async Update":
// Render
// Render
// → 2 renders! Performance issue!React 18 (Automatic Batching everywhere):
/**
* Demo: React 18 automatic batching
* Batching hoạt động ở MỌI NƠI
*/
function Counter18() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
console.log('Render');
// ✅ Event handler: Batched
const handleClick = () => {
setCount((c) => c + 1);
setFlag((f) => !f);
// React 18: Batched → 1 render
};
// ✅ setTimeout: Batched! (NEW in React 18)
const handleAsync = () => {
setTimeout(() => {
setCount((c) => c + 1);
setFlag((f) => !f);
// React 18: Batched → 1 render ✨
}, 1000);
};
// ✅ fetch: Batched! (NEW in React 18)
const handleFetch = async () => {
const data = await fetch('/api/data');
setCount((c) => c + 1);
setFlag((f) => !f);
// React 18: Batched → 1 render ✨
};
return (
<div>
<p>
Count: {count}, Flag: {flag.toString()}
</p>
<button onClick={handleClick}>Sync Update (1 render)</button>
<button onClick={handleAsync}>Async Update (1 render!)</button>
<button onClick={handleFetch}>Fetch Update (1 render!)</button>
</div>
);
}
// Console output khi click "Async Update":
// Render
// → Chỉ 1 render! Performance improved! ✨⚠️ Opt-out nếu cần:
import { flushSync } from 'react-dom';
function OptOut() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
const handleClick = () => {
flushSync(() => {
setCount((c) => c + 1); // Render immediately
});
flushSync(() => {
setFlag((f) => !f); // Render immediately
});
// Total: 2 renders (opted out of batching)
};
return <button onClick={handleClick}>Force 2 Renders</button>;
}Demo 2: Visualizing Concurrent Rendering ⭐⭐
/**
* Demo: Measure render performance với profiler
* So sánh sync vs concurrent rendering impact
*/
import { Profiler } from 'react';
function PerformanceDemo() {
const [items, setItems] = useState(generateItems(5000));
const [query, setQuery] = useState('');
// Callback để measure render time
const onRenderCallback = (
id,
phase, // "mount" hoặc "update"
actualDuration, // Time spent rendering
baseDuration, // Estimated time without memoization
startTime,
commitTime,
interactions,
) => {
console.log(`${id} ${phase} phase:`);
console.log(` Actual time: ${actualDuration.toFixed(2)}ms`);
console.log(` Base time: ${baseDuration.toFixed(2)}ms`);
};
const handleChange = (e) => {
const value = e.target.value;
setQuery(value);
// Heavy filtering
const filtered = items.filter((item) =>
item.name.toLowerCase().includes(value.toLowerCase()),
);
setItems(filtered);
};
return (
<Profiler
id='SearchDemo'
onRender={onRenderCallback}
>
<div>
<input
type='text'
value={query}
onChange={handleChange}
placeholder='Search...'
/>
<div>
{items.map((item) => (
<div key={item.id}>{item.name}</div>
))}
</div>
</div>
</Profiler>
);
}
function generateItems(count) {
return Array.from({ length: count }, (_, i) => ({
id: i,
name: `Item ${i}`,
}));
}
// Console output:
// SearchDemo update phase:
// Actual time: 245.30ms ← Blocking UI!
// Base time: 245.30msDemo 3: React DevTools Profiler ⭐⭐⭐
/**
* Demo: Sử dụng React DevTools để identify performance issues
*
* Cách dùng:
* 1. Mở React DevTools
* 2. Tab "Profiler"
* 3. Click record (⏺️)
* 4. Interact với app
* 5. Stop recording
* 6. Analyze flame graph
*/
function ProfilerExample() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// Heavy component - intentionally slow
const HeavyComponent = () => {
const startTime = performance.now();
while (performance.now() - startTime < 50) {
// Simulate heavy computation
}
return <div>Heavy Component rendered</div>;
};
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder='Type here...'
/>
{/* This will show up red in profiler */}
<HeavyComponent />
{/* Render nhiều components để dễ visualize */}
{Array.from({ length: 100 }).map((_, i) => (
<div key={i}>Item {i}</div>
))}
</div>
);
}
/**
* Flame Graph Analysis:
*
* 🔴 Red/Orange: Took long time (>12ms)
* 🟡 Yellow: Moderate time (4-12ms)
* 🟢 Green: Fast (<4ms)
*
* Ranked Chart: Shows components by render time
* - Focus on top items first
* - Optimize red/orange components
*/🔨 PHẦN 3: BÀI TẬP THỰC HÀNH (60 phút)
⭐ Level 1: Kiểm Tra Automatic Batching (15 phút)
/**
* 🎯 Mục tiêu: Verify automatic batching trong React 18
* ⏱️ Thời gian: 15 phút
* 🚫 KHÔNG dùng: useTransition, useDeferredValue
*
* Requirements:
* 1. Tạo component với 3 state variables
* 2. Update cả 3 states trong setTimeout
* 3. Log render count để verify chỉ 1 render
* 4. So sánh behavior nếu dùng React 17
*
* 💡 Gợi ý: Dùng useRef để track render count
*/
// ❌ Cách SAI (không track được renders):
function BadExample() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
const [c, setC] = useState(0);
// Không có cách nào biết bao nhiêu renders!
return <div>{a + b + c}</div>;
}
// ✅ Cách ĐÚNG (track renders properly):
function GoodExample() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
const [c, setC] = useState(0);
const renderCount = useRef(0);
useEffect(() => {
renderCount.current += 1;
console.log(`Render #${renderCount.current}`);
});
return (
<div>
<p>Sum: {a + b + c}</p>
<p>Render count: {renderCount.current}</p>
</div>
);
}
// 🎯 NHIỆM VỤ CỦA BẠN:
function BatchingTest() {
// TODO: Implement state variables
// TODO: Implement render counter
const handleAsync = () => {
setTimeout(() => {
// TODO: Update all 3 states here
// Expected: Chỉ 1 render trong React 18
}, 100);
};
const handlePromise = () => {
Promise.resolve().then(() => {
// TODO: Update all 3 states here
// Expected: Chỉ 1 render trong React 18
});
};
return (
<div>
{/* TODO: Display states và render count */}
{/* TODO: Add buttons để trigger updates */}
</div>
);
}💡 Solution
/**
* Automatic Batching Test Component
* Verifies React 18 batches updates trong async contexts
*/
function BatchingTest() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
const [text, setText] = useState('');
const renderCount = useRef(0);
useEffect(() => {
renderCount.current += 1;
console.log(`Render #${renderCount.current}`);
});
const handleAsync = () => {
console.log('--- Async update started ---');
setTimeout(() => {
setCount((c) => c + 1);
setFlag((f) => !f);
setText('Updated via setTimeout');
// React 18: 1 render
// React 17: 3 renders
}, 100);
};
const handlePromise = () => {
console.log('--- Promise update started ---');
Promise.resolve().then(() => {
setCount((c) => c + 1);
setFlag((f) => !f);
setText('Updated via Promise');
// React 18: 1 render
// React 17: 3 renders
});
};
const handleFetch = async () => {
console.log('--- Fetch update started ---');
// Simulate fetch
await new Promise((resolve) => setTimeout(resolve, 100));
setCount((c) => c + 1);
setFlag((f) => !f);
setText('Updated via fetch');
// React 18: 1 render
// React 17: 3 renders
};
return (
<div>
<h3>Automatic Batching Test</h3>
<div>
<p>Count: {count}</p>
<p>Flag: {flag.toString()}</p>
<p>Text: {text}</p>
<p>
<strong>Total Renders: {renderCount.current}</strong>
</p>
</div>
<button onClick={handleAsync}>Update via setTimeout</button>
<button onClick={handlePromise}>Update via Promise</button>
<button onClick={handleFetch}>Update via Fetch</button>
</div>
);
}
/**
* Expected results (React 18):
* - Click "Update via setTimeout" → Render count +1
* - Click "Update via Promise" → Render count +1
* - Click "Update via Fetch" → Render count +1
*
* In React 17:
* - Each click would increase render count by +3
*/⭐⭐ Level 2: Performance Profiling (25 phút)
/**
* 🎯 Mục tiêu: Measure và visualize render performance
* ⏱️ Thời gian: 25 phút
*
* Scenario: Build một search interface với large dataset
*
* 🤔 PHÂN TÍCH:
* Approach A: Filter inline trong render
* Pros: Simple, straightforward
* Cons: Recalculates every render
*
* Approach B: Filter trong event handler
* Pros: Better performance
* Cons: Multiple state updates
*
* Approach C: useMemo để cache filtered results
* Pros: Optimal performance
* Cons: More complex
*
* 💭 BẠN CHỌN GÌ VÀ TẠI SAO?
*
* Requirements:
* 1. Generate 10,000 items
* 2. Implement search với cả 3 approaches
* 3. Dùng Profiler component để measure
* 4. Log render times
* 5. So sánh performance
*/
// 🎯 NHIỆM VỤ CỦA BẠN:
function SearchPerformance() {
const allItems = useMemo(
() =>
Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `Item ${i}`,
category: ['A', 'B', 'C'][i % 3],
})),
[],
);
// TODO: Implement 3 approaches
// TODO: Add Profiler to measure each
// TODO: Display render times
return <div>{/* Your implementation */}</div>;
}💡 Solution
/**
* Search Performance Comparison
* Compares different filtering approaches
*/
function SearchPerformance() {
const allItems = useMemo(
() =>
Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `Item ${i}`,
category: ['Electronics', 'Clothing', 'Food'][i % 3],
})),
[],
);
const [approach, setApproach] = useState('A');
const [times, setTimes] = useState({ A: [], B: [], C: [] });
const onRender = useCallback((id, phase, actualDuration) => {
if (phase === 'update') {
setTimes((prev) => ({
...prev,
[id]: [...prev[id].slice(-4), actualDuration],
}));
}
}, []);
const avgTime = (arr) =>
arr.length
? (arr.reduce((a, b) => a + b, 0) / arr.length).toFixed(2)
: '0.00';
return (
<div>
<h3>Performance Comparison</h3>
<div style={{ marginBottom: 20 }}>
<button onClick={() => setApproach('A')}>Approach A</button>
<button onClick={() => setApproach('B')}>Approach B</button>
<button onClick={() => setApproach('C')}>Approach C</button>
</div>
<div style={{ marginBottom: 20 }}>
<p>Approach A avg: {avgTime(times.A)}ms (Filter inline)</p>
<p>Approach B avg: {avgTime(times.B)}ms (Filter in handler)</p>
<p>Approach C avg: {avgTime(times.C)}ms (useMemo)</p>
</div>
{approach === 'A' && (
<Profiler
id='A'
onRender={onRender}
>
<ApproachA items={allItems} />
</Profiler>
)}
{approach === 'B' && (
<Profiler
id='B'
onRender={onRender}
>
<ApproachB items={allItems} />
</Profiler>
)}
{approach === 'C' && (
<Profiler
id='C'
onRender={onRender}
>
<ApproachC items={allItems} />
</Profiler>
)}
</div>
);
}
/**
* Approach A: Filter inline (WORST)
* Re-filters on EVERY render, even unrelated updates
*/
function ApproachA({ items }) {
const [query, setQuery] = useState('');
const [count, setCount] = useState(0); // Unrelated state
// ❌ Filters on every render!
const filtered = items.filter((item) =>
item.name.toLowerCase().includes(query.toLowerCase()),
);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder='Search...'
/>
<button onClick={() => setCount((c) => c + 1)}>
Unrelated update: {count}
</button>
<p>Found: {filtered.length} items</p>
</div>
);
}
/**
* Approach B: Filter in handler (BETTER)
* Only filters when query changes
*/
function ApproachB({ items }) {
const [query, setQuery] = useState('');
const [filtered, setFiltered] = useState(items);
const [count, setCount] = useState(0);
const handleSearch = (e) => {
const value = e.target.value;
setQuery(value);
// Filter and update state
const results = items.filter((item) =>
item.name.toLowerCase().includes(value.toLowerCase()),
);
setFiltered(results);
};
return (
<div>
<input
value={query}
onChange={handleSearch}
placeholder='Search...'
/>
<button onClick={() => setCount((c) => c + 1)}>
Unrelated update: {count}
</button>
<p>Found: {filtered.length} items</p>
</div>
);
}
/**
* Approach C: useMemo (BEST)
* Only recalculates when dependencies change
*/
function ApproachC({ items }) {
const [query, setQuery] = useState('');
const [count, setCount] = useState(0);
// ✅ Only recalculates when query or items change
const filtered = useMemo(() => {
console.log('Filtering...'); // Log để verify
return items.filter((item) =>
item.name.toLowerCase().includes(query.toLowerCase()),
);
}, [items, query]);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder='Search...'
/>
<button onClick={() => setCount((c) => c + 1)}>
Unrelated update: {count}
</button>
<p>Found: {filtered.length} items</p>
</div>
);
}
/**
* Performance results (typical):
* Approach A: 15-20ms per render (includes filtering)
* Approach B: 2-3ms per render (no filtering on unrelated updates)
* Approach C: 2-3ms per render (memoized)
*
* Key insight: Approach B and C similar, but C is cleaner code
*/⭐⭐⭐ Level 3: React DevTools Profiler Analysis (40 phút)
/**
* 🎯 Mục tiêu: Identify và fix performance bottlenecks
* ⏱️ Thời gian: 40 phút
*
* 📋 Product Requirements:
* User Story: "Là user, tôi muốn type trong search box mượt mà
* không bị lag, ngay cả khi có nhiều kết quả"
*
* ✅ Acceptance Criteria:
* - [ ] Search không lag khi typing nhanh
* - [ ] Unrelated updates không re-render search results
* - [ ] Profiler shows minimal wasted renders
*
* 🎨 Technical Constraints:
* - Dataset: 5000 items
* - Target: <16ms per render (60fps)
* - Must show loading indicator
*
* 🚨 Edge Cases cần handle:
* - Empty search (show all)
* - No results found
* - Rapid typing (debounce?)
*
* 📝 Implementation Checklist:
* - [ ] Implement buggy version first
* - [ ] Use Profiler to identify issues
* - [ ] Apply optimizations
* - [ ] Measure before/after
*/
// 🎯 NHIỆM VỤ CỦA BẠN:
function ProductSearch() {
// TODO: Implement search với intentional performance issues
// TODO: Add Profiler
// TODO: Identify bottlenecks
// TODO: Fix issues
// TODO: Document findings
}💡 Solution
/**
* Product Search - Performance Optimization Exercise
* Step-by-step optimization với profiling
*/
// Step 1: Buggy version với multiple issues
function ProductSearchBuggy() {
const [query, setQuery] = useState('');
const [category, setCategory] = useState('all');
const [sortBy, setSortBy] = useState('name');
// ❌ Issue 1: Generate data on every render!
const allProducts = Array.from({ length: 5000 }, (_, i) => ({
id: i,
name: `Product ${i}`,
category: ['Electronics', 'Clothing', 'Food'][i % 3],
price: Math.floor(Math.random() * 1000),
}));
// ❌ Issue 2: No memoization
const filtered = allProducts.filter((p) => {
const matchQuery = p.name.toLowerCase().includes(query.toLowerCase());
const matchCategory = category === 'all' || p.category === category;
return matchQuery && matchCategory;
});
// ❌ Issue 3: Sorting on every render
const sorted = [...filtered].sort((a, b) => {
if (sortBy === 'name') return a.name.localeCompare(b.name);
return a.price - b.price;
});
return (
<div>
<h3>❌ Buggy Version (measure this!)</h3>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder='Search products...'
/>
<select
value={category}
onChange={(e) => setCategory(e.target.value)}
>
<option value='all'>All Categories</option>
<option value='Electronics'>Electronics</option>
<option value='Clothing'>Clothing</option>
<option value='Food'>Food</option>
</select>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
>
<option value='name'>Sort by Name</option>
<option value='price'>Sort by Price</option>
</select>
<p>Found: {sorted.length} products</p>
{/* ❌ Issue 4: No memo, re-renders always */}
{sorted.slice(0, 100).map((product) => (
<ProductCard
key={product.id}
product={product}
/>
))}
</div>
);
}
// Heavy component without memo
function ProductCard({ product }) {
// Simulate heavy render
const startTime = performance.now();
while (performance.now() - startTime < 1) {
// Busy wait 1ms
}
return (
<div style={{ padding: 8, border: '1px solid #ddd', margin: 4 }}>
<strong>{product.name}</strong> - ${product.price}
</div>
);
}
// Step 2: Optimized version
function ProductSearchOptimized() {
const [query, setQuery] = useState('');
const [category, setCategory] = useState('all');
const [sortBy, setSortBy] = useState('name');
const [renderTime, setRenderTime] = useState(0);
// ✅ Fix 1: useMemo for data generation
const allProducts = useMemo(
() =>
Array.from({ length: 5000 }, (_, i) => ({
id: i,
name: `Product ${i}`,
category: ['Electronics', 'Clothing', 'Food'][i % 3],
price: Math.floor(Math.random() * 1000),
})),
[],
);
// ✅ Fix 2: useMemo for filtering
const filtered = useMemo(() => {
console.log('Filtering...');
return allProducts.filter((p) => {
const matchQuery = p.name.toLowerCase().includes(query.toLowerCase());
const matchCategory = category === 'all' || p.category === category;
return matchQuery && matchCategory;
});
}, [allProducts, query, category]);
// ✅ Fix 3: useMemo for sorting
const sorted = useMemo(() => {
console.log('Sorting...');
return [...filtered].sort((a, b) => {
if (sortBy === 'name') return a.name.localeCompare(b.name);
return a.price - b.price;
});
}, [filtered, sortBy]);
const onRender = useCallback((id, phase, actualDuration) => {
if (phase === 'update') {
setRenderTime(actualDuration);
}
}, []);
return (
<Profiler
id='ProductSearch'
onRender={onRender}
>
<div>
<h3>✅ Optimized Version</h3>
<p>Last render: {renderTime.toFixed(2)}ms</p>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder='Search products...'
/>
<select
value={category}
onChange={(e) => setCategory(e.target.value)}
>
<option value='all'>All Categories</option>
<option value='Electronics'>Electronics</option>
<option value='Clothing'>Clothing</option>
<option value='Food'>Food</option>
</select>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
>
<option value='name'>Sort by Name</option>
<option value='price'>Sort by Price</option>
</select>
<p>Found: {sorted.length} products</p>
{/* ✅ Fix 4: Memo'd component */}
{sorted.slice(0, 100).map((product) => (
<ProductCardMemo
key={product.id}
product={product}
/>
))}
</div>
</Profiler>
);
}
// ✅ Memoized component
const ProductCardMemo = React.memo(function ProductCard({ product }) {
const startTime = performance.now();
while (performance.now() - startTime < 1) {
// Busy wait 1ms
}
return (
<div style={{ padding: 8, border: '1px solid #ddd', margin: 4 }}>
<strong>{product.name}</strong> - ${product.price}
</div>
);
});
/**
* Performance comparison:
*
* Buggy version:
* - Initial render: ~600ms (regenerates data + sorts)
* - Typing in search: ~300ms per keystroke
* - Changing category: ~300ms
* - Total wasted renders: High
*
* Optimized version:
* - Initial render: ~150ms (memoized data)
* - Typing in search: ~50ms per keystroke
* - Changing category: ~30ms
* - Total wasted renders: Minimal
*
* Improvement: ~6x faster! ✨
*/⭐⭐⭐⭐ Level 4: Custom Performance Monitor (60 phút)
/**
* 🎯 Mục tiêu: Build reusable performance monitoring tool
* ⏱️ Thời gian: 60 phút
*
* 🏗️ PHASE 1: Research & Design (20 phút)
*
* Nhiệm vụ:
* 1. Design API cho usePerformance hook
* 2. Decide metrics cần track
* 3. Visualization strategy
*
* ADR Template:
* - Context: Need to monitor render performance across app
* - Decision: Custom hook + visual indicator
* - Rationale: Reusable, non-invasive, informative
* - Consequences: Small overhead, dev-only
* - Alternatives: React DevTools (manual), console.log (poor UX)
*
* 💻 PHASE 2: Implementation (30 phút)
* [Implement solution]
*
* 🧪 PHASE 3: Testing (10 phút)
* - [ ] Test với fast component (<5ms)
* - [ ] Test với slow component (>50ms)
* - [ ] Verify không ảnh hưởng production
*/
// 🎯 NHIỆM VỤ CỦA BẠN:
function usePerformance(componentName) {
// TODO: Track render count
// TODO: Track render time
// TODO: Track average time
// TODO: Detect slow renders (>16ms)
// TODO: Return metrics và controls
}
function PerformanceMonitor({ children }) {
// TODO: Visual indicator component
// TODO: Show metrics overlay
// TODO: Warning for slow renders
}💡 Solution
/**
* Custom Performance Monitor Hook & Component
* Tracks và visualizes component performance
*/
/**
* usePerformance - Track component render metrics
* @param {string} componentName - Tên component để identify
* @returns {Object} Metrics và helper functions
*/
function usePerformance(componentName) {
const renderCount = useRef(0);
const renderTimes = useRef([]);
const slowRenders = useRef([]);
const [metrics, setMetrics] = useState({
count: 0,
avgTime: 0,
lastTime: 0,
slowCount: 0,
});
useEffect(() => {
const startTime = performance.now();
return () => {
const endTime = performance.now();
const renderTime = endTime - startTime;
// Update counts
renderCount.current += 1;
renderTimes.current.push(renderTime);
// Keep last 100 renders
if (renderTimes.current.length > 100) {
renderTimes.current.shift();
}
// Track slow renders (>16ms = 60fps threshold)
if (renderTime > 16) {
slowRenders.current.push({
count: renderCount.current,
time: renderTime,
timestamp: Date.now(),
});
console.warn(
`[Performance] Slow render in ${componentName}: ${renderTime.toFixed(2)}ms`,
);
}
// Calculate metrics
const avgTime =
renderTimes.current.reduce((a, b) => a + b, 0) /
renderTimes.current.length;
setMetrics({
count: renderCount.current,
avgTime,
lastTime: renderTime,
slowCount: slowRenders.current.length,
});
};
});
const reset = useCallback(() => {
renderCount.current = 0;
renderTimes.current = [];
slowRenders.current = [];
setMetrics({ count: 0, avgTime: 0, lastTime: 0, slowCount: 0 });
}, []);
const getSlowRenders = useCallback(() => {
return slowRenders.current;
}, []);
return {
metrics,
reset,
getSlowRenders,
};
}
/**
* PerformanceMonitor - Visual performance indicator
* Shows performance metrics in overlay
*/
function PerformanceMonitor({
componentName,
children,
threshold = 16, // ms - 60fps threshold
}) {
const { metrics, reset, getSlowRenders } = usePerformance(componentName);
const [showDetails, setShowDetails] = useState(false);
const status = metrics.avgTime > threshold ? 'slow' : 'fast';
const color = status === 'slow' ? '#ef4444' : '#10b981';
return (
<div style={{ position: 'relative' }}>
{/* Performance badge */}
<div
onClick={() => setShowDetails(!showDetails)}
style={{
position: 'absolute',
top: 0,
right: 0,
backgroundColor: color,
color: 'white',
padding: '4px 8px',
borderRadius: 4,
fontSize: 12,
cursor: 'pointer',
zIndex: 1000,
userSelect: 'none',
}}
>
{metrics.lastTime.toFixed(1)}ms
</div>
{/* Detailed metrics panel */}
{showDetails && (
<div
style={{
position: 'absolute',
top: 30,
right: 0,
backgroundColor: 'white',
border: '1px solid #ccc',
borderRadius: 4,
padding: 12,
fontSize: 12,
zIndex: 1001,
minWidth: 200,
boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
}}
>
<h4 style={{ margin: '0 0 8px 0' }}>{componentName}</h4>
<div style={{ marginBottom: 8 }}>
<strong>Renders:</strong> {metrics.count}
</div>
<div style={{ marginBottom: 8 }}>
<strong>Avg time:</strong> {metrics.avgTime.toFixed(2)}ms
</div>
<div style={{ marginBottom: 8 }}>
<strong>Last render:</strong> {metrics.lastTime.toFixed(2)}ms
</div>
<div style={{ marginBottom: 8 }}>
<strong>Slow renders:</strong> {metrics.slowCount}
</div>
{metrics.slowCount > 0 && (
<div
style={{
marginTop: 8,
padding: 8,
backgroundColor: '#fef2f2',
borderRadius: 4,
}}
>
<strong>⚠️ Optimization needed!</strong>
<div style={{ marginTop: 4, fontSize: 11 }}>
{getSlowRenders()
.slice(-3)
.map((render, i) => (
<div key={i}>
Render #{render.count}: {render.time.toFixed(2)}ms
</div>
))}
</div>
</div>
)}
<button
onClick={reset}
style={{
marginTop: 8,
width: '100%',
padding: '4px 8px',
fontSize: 12,
cursor: 'pointer',
}}
>
Reset Metrics
</button>
</div>
)}
{children}
</div>
);
}
// Example usage:
function DemoApp() {
const [count, setCount] = useState(0);
const [items, setItems] = useState(Array.from({ length: 1000 }, (_, i) => i));
return (
<div>
<h2>Performance Monitor Demo</h2>
{/* Fast component */}
<PerformanceMonitor componentName='FastComponent'>
<FastComponent count={count} />
</PerformanceMonitor>
{/* Slow component */}
<PerformanceMonitor
componentName='SlowComponent'
threshold={10}
>
<SlowComponent items={items} />
</PerformanceMonitor>
<button onClick={() => setCount((c) => c + 1)}>Increment Count</button>
<button onClick={() => setItems((items) => [...items, items.length])}>
Add Item
</button>
</div>
);
}
function FastComponent({ count }) {
return <div>Fast component: {count}</div>;
}
function SlowComponent({ items }) {
// Intentionally slow
const startTime = performance.now();
while (performance.now() - startTime < 20) {
// Busy wait
}
return (
<div>
<p>Slow component with {items.length} items</p>
{items.slice(0, 10).map((i) => (
<div key={i}>Item {i}</div>
))}
</div>
);
}
/**
* Key features:
* 1. Visual badge showing last render time
* 2. Color-coded (green = fast, red = slow)
* 3. Click to show detailed metrics
* 4. Tracks slow renders with warnings
* 5. Reset functionality
* 6. Configurable threshold
*
* Usage tips:
* - Wrap components you want to monitor
* - Check for red badges (slow renders)
* - Click badge to see detailed metrics
* - Use in development only (add process.env.NODE_ENV check)
*/⭐⭐⭐⭐⭐ Level 5: Performance Dashboard (90 phút)
/**
* 🎯 Mục tiêu: Build comprehensive performance monitoring dashboard
* ⏱️ Thời gian: 90 phút
*
* 📋 Feature Specification:
* Build a developer tool để monitor toàn bộ app performance
* - Real-time render metrics
* - Component hierarchy visualization
* - Slow render alerts
* - Export metrics
*
* 🏗️ Technical Design Doc:
* 1. Component Architecture
* - PerformanceProvider (context)
* - usePerformanceMonitor hook
* - PerformanceDashboard UI
* - MetricsChart visualization
*
* 2. State Management Strategy
* - Context cho global metrics
* - Local state cho UI controls
*
* 3. Performance Considerations
* - Minimal overhead (<1ms)
* - Debounced updates
* - Memory limits (max 1000 data points)
*
* 4. Error Handling Strategy
* - Graceful degradation nếu không support
* - Try-catch cho measurement APIs
*
* ✅ Production Checklist:
* - [ ] DEV-only (check NODE_ENV)
* - [ ] No production bundle impact
* - [ ] Keyboard shortcut (Cmd/Ctrl + Shift + P)
* - [ ] Draggable window
* - [ ] Local storage persistence
* - [ ] Export CSV functionality
* - [ ] Clear metrics function
*/
// 🎯 NHIỆM VỤ CỦA BẠN:
// Implement full performance monitoring solution💡 Solution
/**
* Production-ready Performance Dashboard
* Comprehensive monitoring tool cho React apps
*/
// Context for global performance tracking
const PerformanceContext = React.createContext(null);
/**
* PerformanceProvider - Wrap app để track metrics
*/
function PerformanceProvider({ children }) {
const [components, setComponents] = useState(new Map());
const [showDashboard, setShowDashboard] = useState(false);
// Keyboard shortcut: Cmd/Ctrl + Shift + P
useEffect(() => {
const handleKeyPress = (e) => {
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'P') {
e.preventDefault();
setShowDashboard((prev) => !prev);
}
};
window.addEventListener('keydown', handleKeyPress);
return () => window.removeEventListener('keydown', handleKeyPress);
}, []);
const registerComponent = useCallback((name, metrics) => {
setComponents((prev) => {
const newMap = new Map(prev);
newMap.set(name, {
...metrics,
timestamp: Date.now(),
});
return newMap;
});
}, []);
const clearMetrics = useCallback(() => {
setComponents(new Map());
}, []);
const exportMetrics = useCallback(() => {
const data = Array.from(components.entries()).map(([name, metrics]) => ({
component: name,
...metrics,
}));
const csv = [
'Component,Renders,AvgTime,LastTime,SlowRenders',
...data.map(
(d) =>
`${d.component},${d.count},${d.avgTime.toFixed(2)},${d.lastTime.toFixed(2)},${d.slowCount}`,
),
].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-${Date.now()}.csv`;
a.click();
}, [components]);
const value = {
components,
registerComponent,
clearMetrics,
exportMetrics,
};
return (
<PerformanceContext.Provider value={value}>
{children}
{showDashboard && (
<PerformanceDashboard onClose={() => setShowDashboard(false)} />
)}
</PerformanceContext.Provider>
);
}
/**
* useComponentPerformance - Hook để track individual component
*/
function useComponentPerformance(componentName) {
const context = React.useContext(PerformanceContext);
const renderCount = useRef(0);
const renderTimes = useRef([]);
const slowRenders = useRef(0);
useEffect(() => {
const startTime = performance.now();
return () => {
const renderTime = performance.now() - startTime;
renderCount.current += 1;
renderTimes.current.push(renderTime);
// Keep last 100
if (renderTimes.current.length > 100) {
renderTimes.current.shift();
}
if (renderTime > 16) {
slowRenders.current += 1;
}
const avgTime =
renderTimes.current.reduce((a, b) => a + b, 0) /
renderTimes.current.length;
context?.registerComponent(componentName, {
count: renderCount.current,
avgTime,
lastTime: renderTime,
slowCount: slowRenders.current,
});
};
});
}
/**
* PerformanceDashboard - Main dashboard UI
*/
function PerformanceDashboard({ onClose }) {
const { components, clearMetrics, exportMetrics } =
React.useContext(PerformanceContext);
const [position, setPosition] = useState({ x: 20, y: 20 });
const [isDragging, setIsDragging] = useState(false);
const dragStart = useRef({ x: 0, y: 0 });
const componentsArray = Array.from(components.entries())
.map(([name, metrics]) => ({ name, ...metrics }))
.sort((a, b) => b.avgTime - a.avgTime);
const totalRenders = componentsArray.reduce((sum, c) => sum + c.count, 0);
const slowComponents = componentsArray.filter((c) => c.avgTime > 16).length;
// Dragging logic
const handleMouseDown = (e) => {
if (e.target.closest('.dashboard-header')) {
setIsDragging(true);
dragStart.current = {
x: e.clientX - position.x,
y: e.clientY - position.y,
};
}
};
useEffect(() => {
if (!isDragging) return;
const handleMouseMove = (e) => {
setPosition({
x: e.clientX - dragStart.current.x,
y: e.clientY - dragStart.current.y,
});
};
const handleMouseUp = () => setIsDragging(false);
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleMouseUp);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
};
}, [isDragging]);
return (
<div
style={{
position: 'fixed',
left: position.x,
top: position.y,
width: 600,
maxHeight: '80vh',
backgroundColor: 'white',
border: '1px solid #ccc',
borderRadius: 8,
boxShadow: '0 4px 20px rgba(0,0,0,0.2)',
zIndex: 10000,
overflow: 'hidden',
fontFamily: 'monospace',
fontSize: 12,
cursor: isDragging ? 'grabbing' : 'default',
}}
onMouseDown={handleMouseDown}
>
{/* Header */}
<div
className='dashboard-header'
style={{
padding: 12,
backgroundColor: '#3b82f6',
color: 'white',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
cursor: 'grab',
}}
>
<h3 style={{ margin: 0 }}>⚡ Performance Dashboard</h3>
<button
onClick={onClose}
style={{
background: 'none',
border: 'none',
color: 'white',
cursor: 'pointer',
fontSize: 18,
}}
>
×
</button>
</div>
{/* Summary */}
<div style={{ padding: 12, borderBottom: '1px solid #e5e7eb' }}>
<div
style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr 1fr',
gap: 12,
}}
>
<div>
<div style={{ color: '#6b7280' }}>Components</div>
<div style={{ fontSize: 20, fontWeight: 'bold' }}>
{components.size}
</div>
</div>
<div>
<div style={{ color: '#6b7280' }}>Total Renders</div>
<div style={{ fontSize: 20, fontWeight: 'bold' }}>
{totalRenders}
</div>
</div>
<div>
<div style={{ color: '#6b7280' }}>Slow Components</div>
<div
style={{
fontSize: 20,
fontWeight: 'bold',
color: slowComponents > 0 ? '#ef4444' : '#10b981',
}}
>
{slowComponents}
</div>
</div>
</div>
</div>
{/* Actions */}
<div
style={{
padding: 12,
borderBottom: '1px solid #e5e7eb',
display: 'flex',
gap: 8,
}}
>
<button
onClick={clearMetrics}
style={{ flex: 1, padding: 6 }}
>
Clear Metrics
</button>
<button
onClick={exportMetrics}
style={{ flex: 1, padding: 6 }}
>
Export CSV
</button>
</div>
{/* Component list */}
<div
style={{
maxHeight: 400,
overflowY: 'auto',
padding: 12,
}}
>
{componentsArray.length === 0 ? (
<div style={{ textAlign: 'center', padding: 20, color: '#6b7280' }}>
No components tracked yet
</div>
) : (
componentsArray.map(
({ name, count, avgTime, lastTime, slowCount }) => (
<div
key={name}
style={{
padding: 8,
marginBottom: 8,
backgroundColor: avgTime > 16 ? '#fef2f2' : '#f9fafb',
borderLeft: `3px solid ${avgTime > 16 ? '#ef4444' : '#10b981'}`,
borderRadius: 4,
}}
>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
marginBottom: 4,
}}
>
<strong>{name}</strong>
<span
style={{
color: avgTime > 16 ? '#ef4444' : '#10b981',
fontWeight: 'bold',
}}
>
{avgTime.toFixed(2)}ms avg
</span>
</div>
<div
style={{
display: 'flex',
gap: 16,
color: '#6b7280',
fontSize: 11,
}}
>
<span>Renders: {count}</span>
<span>Last: {lastTime.toFixed(2)}ms</span>
{slowCount > 0 && (
<span style={{ color: '#ef4444' }}>Slow: {slowCount}</span>
)}
</div>
</div>
),
)
)}
</div>
{/* Footer */}
<div
style={{
padding: 8,
borderTop: '1px solid #e5e7eb',
backgroundColor: '#f9fafb',
fontSize: 10,
color: '#6b7280',
textAlign: 'center',
}}
>
Press Cmd/Ctrl + Shift + P to toggle • Drag to move
</div>
</div>
);
}
// Example: Wrap your app
function App() {
return (
<PerformanceProvider>
<MyApp />
</PerformanceProvider>
);
}
// Example: Track a component
function MyComponent() {
useComponentPerformance('MyComponent');
return <div>My Component</div>;
}
/**
* Features implemented:
* ✅ Global performance tracking via Context
* ✅ Keyboard shortcut (Cmd/Ctrl + Shift + P)
* ✅ Draggable window
* ✅ Real-time metrics
* ✅ Color-coded slow components
* ✅ Export to CSV
* ✅ Clear metrics
* ✅ Summary statistics
* ✅ Sorted by avg time
*
* Production considerations:
* - Wrap with process.env.NODE_ENV === 'development' check
* - Tree-shake in production build
* - Add memory limits (max components tracked)
* - Debounce updates for better performance
*/📊 PHẦN 4: SO SÁNH PATTERNS (30 phút)
Bảng So Sánh: React 17 vs React 18
| Feature | React 17 | React 18 | Impact |
|---|---|---|---|
| Batching | Chỉ trong event handlers | Mọi nơi (automatic) | ⬆️ Performance |
| Rendering | Synchronous | Concurrent (opt-in) | ⬆️ Responsiveness |
| APIs | Legacy | Modern (useTransition, etc) | ⬆️ Developer Experience |
| SSR | Client hydration blocking | Streaming SSR | ⬆️ TTI (Time to Interactive) |
| Suspense | Limited | Full support | ⬆️ Loading UX |
Trade-offs Matrix
| Approach | Pros | Cons | When to Use |
|---|---|---|---|
| React 17 | ✅ Stable ✅ Well-tested ✅ Simple model | ❌ No automatic batching ❌ Blocking renders ❌ No concurrent features | Legacy apps Simple UIs Low interactivity |
| React 18 | ✅ Better performance ✅ Concurrent rendering ✅ Automatic batching ✅ Modern features | ❌ Learning curve ❌ Migration effort ❌ Some breaking changes | New projects High interactivity Performance-critical |
Decision Tree: Upgrade to React 18?
START: Should I upgrade to React 18?
│
├─ App cần Concurrent features? (useTransition, Suspense)
│ ├─ YES → ✅ Upgrade
│ └─ NO → Continue
│
├─ Performance issues với frequent state updates?
│ ├─ YES → ✅ Upgrade (automatic batching helps)
│ └─ NO → Continue
│
├─ Using Suspense for data fetching?
│ ├─ YES → ✅ Upgrade (better support)
│ └─ NO → Continue
│
├─ App is stable, no issues?
│ ├─ YES → ⚠️ Optional (low risk, minor benefits)
│ └─ NO → Continue
│
└─ Legacy codebase, high risk?
└─ NO → ⚠️ Test thoroughly before upgradeBatching Comparison
// SCENARIO: Multiple state updates trong async callback
// ❌ React 17
setTimeout(() => {
setA(1); // Render 1
setB(2); // Render 2
setC(3); // Render 3
}, 100);
// Total: 3 renders → Performance issue!
// ✅ React 18
setTimeout(() => {
setA(1);
setB(2);
setC(3);
}, 100);
// Total: 1 render → Optimized! ✨
// WHEN TO OPT-OUT (rare cases):
import { flushSync } from 'react-dom';
setTimeout(() => {
flushSync(() => setA(1)); // Render immediately
flushSync(() => setB(2)); // Render immediately
setC(3); // Batched with next update
}, 100);
// Use case: Need immediate DOM update for measurements🧪 PHẦN 5: DEBUG LAB (20 phút)
Bug 1: Expecting Automatic Performance Improvements
/**
* ❌ BUG: Upgraded to React 18 nhưng vẫn chậm
*
* Symptom: UI vẫn lag khi typing, không có improvement
* Root cause: Concurrent features are OPT-IN, không tự động
*/
function SlowSearch() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const handleSearch = (e) => {
const value = e.target.value;
setQuery(value);
// ❌ Heavy sync operation vẫn block UI
const filtered = heavyFilter(allData, value);
setResults(filtered);
};
return (
<input
value={query}
onChange={handleSearch}
/>
);
}
// ❓ CÂU HỎI:
// 1. Tại sao React 18 không tự động làm nhanh hơn?
// 2. Solution là gì?
// 3. Khi nào dùng useTransition? (sẽ học ngày sau)💡 Giải thích:
Tại sao không tự động nhanh hơn?
- Concurrent rendering là OPT-IN
- Cần dùng
useTransitionhoặcuseDeferredValue - Heavy computation vẫn cần optimize (memoization, code splitting)
Solutions:
jsx// Solution 1: useMemo để tránh re-compute const filtered = useMemo(() => heavyFilter(allData, query), [query]); // Solution 2: useTransition (sẽ học Ngày 47) // const [isPending, startTransition] = useTransition(); // startTransition(() => setResults(filtered)); // Solution 3: Web Worker (advanced) // Offload to background threadKhi nào dùng useTransition:
- Update không urgent (search results, filtering)
- User cần feedback ngay (input field)
- Heavy computation có thể defer
Bug 2: Infinite Renders sau Upgrade
/**
* ❌ BUG: Component re-render vô hạn sau upgrade React 18
*
* Symptom: Browser freeze, console đầy "Render"
* Root cause: useEffect dependency issue amplified bởi automatic batching
*/
function BuggyComponent() {
const [data, setData] = useState([]);
const [filter, setFilter] = useState('all');
console.log('Render');
useEffect(() => {
// ❌ Creates new array reference every time
const filtered = data.filter(
(item) => filter === 'all' || item.type === filter,
);
setData(filtered); // This triggers re-render!
}, [data, filter]); // data changes → effect runs → setData → data changes...
return <div>{data.length} items</div>;
}
// ❓ CÂU HỎI:
// 1. Tại sao infinite loop xảy ra?
// 2. React 17 có bug này không? Tại sao React 18 amplify?
// 3. Fix như thế nào?💡 Giải thích:
Tại sao infinite loop:
useEffectdepends ondata- Effect runs →
setData(filtered)→datachanges datachanges → effect runs again → infinite loop!
React 17 vs 18:
- React 17: Bug vẫn có nhưng có thể ít obvious hơn
- React 18: Automatic batching làm loop nhanh hơn → dễ phát hiện
Fix:
jsx// ✅ Fix 1: Remove data từ dependencies useEffect(() => { setData((prev) => prev.filter((item) => filter === 'all' || item.type === filter), ); }, [filter]); // Chỉ depend on filter // ✅ Fix 2: useMemo thay vì useEffect const filteredData = useMemo( () => originalData.filter((item) => filter === 'all' || item.type === filter), [filter], ); // ✅ Fix 3: Separate filtered state const [rawData, setRawData] = useState([]); const [filteredData, setFilteredData] = useState([]); useEffect(() => { setFilteredData( rawData.filter((item) => filter === 'all' || item.type === filter), ); }, [rawData, filter]); // Safe - rawData không update trong effect
Bug 3: flushSync Misuse
/**
* ❌ BUG: Overusing flushSync causing performance regression
*
* Symptom: React 18 chậm hơn React 17!
* Root cause: Opt-out khỏi batching unnecessarily
*/
import { flushSync } from 'react-dom';
function OverFlushSync() {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(false);
const handleLoad = async () => {
// ❌ Unnecessary flushSync
flushSync(() => {
setLoading(true);
});
const data = await fetchData();
// ❌ Another unnecessary flushSync
flushSync(() => {
setItems(data);
});
// ❌ And another!
flushSync(() => {
setLoading(false);
});
};
return (
<button onClick={handleLoad}>{loading ? 'Loading...' : 'Load Data'}</button>
);
}
// ❓ CÂU HỎI:
// 1. Tại sao code này slow?
// 2. Khi nào NÊN dùng flushSync?
// 3. Fix như thế nào?💡 Giải thích:
Tại sao slow:
flushSyncforce synchronous render- Mỗi
flushSynccall = 1 render ngay lập tức - Total: 3 synchronous renders thay vì 1 batched render
- Mất đi benefit của automatic batching!
Khi NÊN dùng flushSync:
jsx// ✅ Use case 1: Cần DOM measurement ngay flushSync(() => { setHeight(100); }); const actualHeight = ref.current.offsetHeight; // Accurate! // ✅ Use case 2: Third-party lib cần sync DOM flushSync(() => { setOpen(true); }); thirdPartyLib.focus(ref.current); // DOM đã update! // ✅ Use case 3: Scroll position restore flushSync(() => { setItems(newItems); }); scrollToPosition(savedPosition);Fix:
jsx// ✅ Let React batch automatically const handleLoad = async () => { setLoading(true); const data = await fetchData(); // React 18 tự động batch cả 2 updates này! setItems(data); setLoading(false); // Total: 1 render ✨ }; // Hoặc nếu muốn explicit: const handleLoad = async () => { setLoading(true); const data = await fetchData(); // Batched update React.startTransition(() => { setItems(data); setLoading(false); }); };
✅ PHẦN 6: TỰ ĐÁNH GIÁ (15 phút)
Knowledge Check
- [ ] Tôi hiểu sự khác biệt giữa Synchronous và Concurrent rendering
- [ ] Tôi biết Automatic Batching hoạt động như thế nào trong React 18
- [ ] Tôi có thể dùng Profiler component để measure performance
- [ ] Tôi hiểu khi nào nên upgrade lên React 18
- [ ] Tôi biết khi nào dùng
flushSyncvà khi nào KHÔNG nên dùng - [ ] Tôi có thể identify performance bottlenecks bằng React DevTools
- [ ] Tôi hiểu Concurrent features là opt-in, không automatic
Code Review Checklist
React 18 Migration:
- [ ] Đã test automatic batching trong async callbacks
- [ ] Verify không có breaking changes trong codebase
- [ ] useEffect dependencies vẫn correct sau upgrade
- [ ] Performance không regression (measure before/after)
Performance Monitoring:
- [ ] Profiler component được dùng đúng cách
- [ ] Metrics tracking không ảnh hưởng production
- [ ] React DevTools được dùng để identify bottlenecks
- [ ] Slow renders được document và track
Best Practices:
- [ ] Không overuse
flushSync - [ ] useMemo/useCallback được dùng khi cần
- [ ] Component không re-render unnecessarily
- [ ] Performance monitoring chỉ trong development
🏠 BÀI TẬP VỀ NHÀ
Bắt buộc (30 phút)
1. Batching Verification Lab:
- Tạo component test automatic batching
- So sánh behavior trong different contexts:
- Event handlers
- setTimeout
- Promises
- fetch callbacks
- Document findings với screenshots
2. Profiler Integration:
- Add Profiler component vào 1 existing project
- Identify top 3 slowest components
- Document render times và potential optimizations
Nâng cao (60 phút)
1. Migration Guide:
- Document step-by-step guide để upgrade project từ React 17 → 18
- Include:
- Breaking changes checklist
- Testing strategy
- Rollback plan
- Performance comparison
2. Performance Audit:
- Chọn 1 component trong project
- Measure với React DevTools Profiler
- Identify performance issues
- Implement optimizations
- Document before/after metrics
📚 TÀI LIỆU THAM KHẢO
Bắt buộc đọc
React 18 Release Notes
- https://react.dev/blog/2022/03/29/react-v18
- Focus: Automatic Batching, Concurrent Features
New in React 18
- https://react.dev/blog/2022/03/08/react-18-upgrade-guide
- Migration guide và breaking changes
Đọc thêm
Concurrent Rendering Deep Dive
Performance Profiling Guide
🔗 KẾT NỐI KIẾN THỨC
Kiến thức nền (cần biết)
- Ngày 11-14: useState và state updates
- Ngày 16-20: useEffect và side effects
- Ngày 31-34: Performance optimization (memo, useMemo, useCallback)
- Profiler API: Đã biết từ demo trước
Hướng tới (sẽ dùng)
- Ngày 47: useTransition - Non-urgent updates
- Ngày 48: useDeferredValue - Deferred values
- Ngày 49: Suspense for Data Fetching
- Ngày 50: Error Boundaries
- Ngày 51: React Server Components
💡 SENIOR INSIGHTS
Cân Nhắc Production
1. Migration Strategy:
PHASED APPROACH:
Week 1: Upgrade dependencies, test builds
Week 2: Test critical paths, fix breaking changes
Week 3: Performance monitoring
Week 4: Gradual rollout (10% → 50% → 100% users)2. Monitoring:
- Track render times before/after upgrade
- Watch for performance regressions
- Monitor error rates
- Set up alerts cho slow renders
3. Rollback Plan:
- Keep React 17 build ready
- Feature flags để toggle concurrent features
- Incremental adoption strategy
Câu Hỏi Phỏng Vấn
Junior Level:
- Q: React 18 có gì mới so với React 17?
- A: Automatic batching everywhere, concurrent rendering opt-in, new APIs (useTransition, useDeferredValue), Suspense improvements
Mid Level:
- Q: Giải thích Automatic Batching và tại sao nó improve performance?
- A: React 18 batches multiple state updates thành 1 render, ngay cả trong async callbacks (setTimeout, promises). Ít renders hơn = better performance. React 17 chỉ batch trong event handlers.
Senior Level:
- Q: Khi nào nên migrate lên React 18? Trade-offs là gì?
- A:
- Migrate khi: Cần concurrent features, performance issues với frequent updates, modern SSR
- Trade-offs: Learning curve, migration effort, potential breaking changes
- Strategy: Test thoroughly, gradual rollout, monitor metrics, rollback plan ready
Architect Level:
- Q: Design strategy để migrate large legacy app lên React 18 without downtime
- A:
- Audit codebase (breaking changes, third-party deps)
- Phased migration (dev → staging → prod)
- Feature flags để toggle concurrent features
- Comprehensive testing (unit, integration, E2E, perf)
- Monitoring setup (render times, error rates)
- Gradual rollout (percentage-based)
- Rollback plan (React 17 build ready)
- Post-migration optimization (adopt new patterns)
War Stories
Story 1: "Automatic Batching Surprise"
Scenario: Upgraded to React 18, performance đột nhiên WORSE!
Root cause: Code rely on synchronous renders để trigger effects
useEffect(() => {
// Expects setA to trigger render before setB
}, [a]);
setA(1); // React 17: Render now
setB(2); // React 17: Render now
setA(1); // React 18: Batched
setB(2); // React 18: Batched → 1 render → effect timing changed!
Fix: Review useEffect dependencies, use flushSync nếu cần sync behaviorStory 2: "The flushSync Trap"
Scenario: Developer đọc về flushSync, nghĩ nó "optimize" performance
Reality: Wrapped mọi setState trong flushSync
→ Opt-out khỏi batching completely
→ React 18 chậm hơn React 17!
Lesson: flushSync là escape hatch cho edge cases, không phải defaultStory 3: "Profiler in Production"
Mistake: Ship Profiler component to production
Impact:
- Bundle size increase
- Runtime overhead (callbacks firing constantly)
- Memory leaks (storing metrics)
Fix:
if (process.env.NODE_ENV === 'development') {
return <Profiler onRender={callback}>{children}</Profiler>;
}
return children;🎯 PREVIEW NGÀY MAI
Ngày 47: useTransition - Non-urgent Updates
Tomorrow sẽ học:
- useTransition hook để mark non-urgent updates
- isPending state để show feedback
- Pattern: Keep UI responsive during heavy updates
- Use case: Search, filtering, sorting large datasets
Prepare:
- Review concurrent rendering concepts hôm nay
- Nghĩ về scenarios trong app của bạn cần "urgent vs non-urgent" updates
- Cài đặt React DevTools mới nhất
See you tomorrow! 🚀