React App Chậm? Đây Là Cách Mình Xử Lý Sau 5 Năm Đau Khổ
Tổng hợp thực chiến các kỹ thuật tối ưu hiệu suất React — từ re-render không cần thiết đến lazy loading, virtualization và build optimization.
Nguyen Nhat Long
@longnn
Bạn mở app lên, click một cái, đợi... đợi... rồi UI mới chịu phản hồi. Quen không? Mình thì quen lắm. Làm việc với một codebase React hơn 200 component, dữ liệu khách hàng tràn ngập, biểu đồ thống kê chi chít — mình đã từng ngồi nhìn Chrome DevTools mà muốn khóc.
Nhưng sau vài năm "chiến đấu", mình rút ra được một bộ checklist khá solid. Hôm nay chia sẻ lại cho anh em.
Trước Khi Optimize: Tìm Đúng Thủ Phạm Đã
Đừng vội nhào vô refactor. Sai lầm lớn nhất mình từng mắc là optimize mò — đoán component nào chậm rồi sửa bừa. Tốn thời gian mà chẳng cải thiện bao nhiêu.
Hãy bắt đầu bằng công cụ:
- React DevTools Profiler: Cái tab Profiler trong React DevTools extension là vũ khí số 1. Record lại một interaction, xem component nào render lâu, render bao nhiêu lần.
- why-did-you-render: Thư viện này sẽ log ra console mỗi khi một component re-render không cần thiết. Bật từng component một, từ trên xuống dưới. Bạn sẽ ngạc nhiên khi thấy có bao nhiêu render vô nghĩa đang xảy ra.
Theo kinh nghiệm của mình, 90% vấn đề hiệu năng React rơi vào 3 nhóm: re-render thừa, bundle quá nặng, và render quá nhiều DOM node.
Re-render Thừa — Kẻ Giết Hiệu Năng Thầm Lặng

Đây là thủ phạm phổ biến nhất. Và gốc rễ thường rất đơn giản:
Truyền object/array mới làm props mỗi lần render
Mỗi khi parent render, nếu bạn viết kiểu này:
1<UserCard style={{ marginTop: 10 }} filters={["active", "premium"]} />
Thì mỗi lần render, một object style mới và một array filters mới được tạo ra. Shallow comparison fail → UserCard re-render dù data không đổi.
Fix: Khai báo bên ngoài component hoặc dùng useMemo:
1const cardStyle = { marginTop: 10 };2const defaultFilters = ["active", "premium"];34function Dashboard() {5 return <UserCard style={cardStyle} filters={defaultFilters} />;6}
Arrow function trong props
Tương tự, onClick={() => handleClick(id)} tạo function mới mỗi render. Dùng useCallback khi cần:
1const handleClick = useCallback((id) => {2 // xử lý3}, []);
Lạm dụng state
Điều mình thấy hay là nhiều bạn nhét mọi thứ vào useState. Nhưng không phải giá trị nào thay đổi cũng cần trigger re-render. Nếu giá trị đó không ảnh hưởng đến UI — dùng useRef:
1// ❌ Gây re-render mỗi lần interval chạy2const [count, setCount] = useState(0);34// ✅ Không gây re-render5const countRef = useRef(0);
Memo hóa component
- Function component: wrap bằng
React.memo() - Class component: extend
PureComponent - Dùng
useMemocho computed values,useCallbackcho functions
Prop Drilling Và State Management
Khi bạn truyền props xuống 5-6 cấp component, không chỉ code xấu mà còn khiến mọi component trung gian re-render khi prop thay đổi.

Giải pháp:
- React Context + useReducer: Đủ dùng cho hầu hết app vừa và nhỏ. Tách context theo domain (UserContext, CartContext...) thay vì một context khổng lồ.
- Redux + Reselect: Với app lớn nhiều dữ liệu phức tạp, Redux vẫn solid. Dùng
reselectđể memoize selector — tránh tính toán lại khi state không liên quan thay đổi. - Zustand, Jotai: Nhẹ hơn Redux nhiều, API đơn giản, đáng thử.
Bundle Nặng — Người Dùng Đợi Download Cả Đống JS
Phần lớn thời gian mình giải quyết vấn đề hiệu năng theo kiểu cổ điển nhất: giảm size.
Code splitting và Lazy loading
1const HeavyChart = React.lazy(() => import('./HeavyChart'));23function Dashboard() {4 return (5 <Suspense fallback={<ChartSkeleton />}>6 <HeavyChart />7 </Suspense>8 );9}
Đừng load component biểu đồ nặng 500KB khi user chưa cần xem. Lazy load nó, kèm skeleton loader để UX vẫn mượt.
Tối ưu assets
- Compress hình ảnh (WebP thay PNG/JPG)
- Tree shaking — đảm bảo webpack/vite loại bỏ code không dùng
- Analyze bundle bằng
webpack-bundle-analyzerđể biết thằng nào đang "ăn" dung lượng
DOM Quá Nhiều — Virtualization Là Cứu Tinh
App của bạn hiển thị danh sách 1000 khách hàng? Đừng render cả 1000 DOM node. Dùng react-window hoặc react-virtualized — chỉ render những item đang visible trên viewport:

1import { FixedSizeList } from 'react-window';23<FixedSizeList height={600} itemCount={1000} itemSize={50} width="100%">4 {({ index, style }) => (5 <div style={style}>6 <CustomerRow data={customers[index]} />7 </div>8 )}9</FixedSizeList>
Từ render 1000 node xuống còn ~15 node visible. Khác biệt cực kỳ lớn.
Tương Tác Nhanh: Scroll, Drag, Hover
Đừng setState trên mỗi scroll event. Dùng throttle/debounce (lodash có sẵn), hoặc tốt hơn — chuyển logic sang CSS:
- Dùng CSS
transformthay vìtop/leftđể animate (tránh layout thrashing) - Dùng CSS custom properties (
--scroll-pos) cập nhật bằng JS, CSS tự handle phần visual - Khai báo
will-change: transformcho element sắp animate
Biểu Đồ Chậm? Bạn Không Đơn Độc
Nếu bạn đang dùng Victory, Recharts, hay bất kỳ thư viện chart nào — chúng thường nặng khi mount. Mình từng gặp case biểu đồ Victory gây lag 2 giây trên mobile.
Trick đơn giản: mount chart sau khi màn hình đã paint xong:
1const [showChart, setShowChart] = useState(false);23useEffect(() => {4 requestAnimationFrame(() => setShowChart(true));5}, []);67return showChart ? <HeavyChart data={data} /> : <ChartSkeleton />;
User thấy skeleton trước, chart load sau — cảm giác nhanh hơn nhiều dù tổng thời gian không đổi. Perception matters.
Checklist Nhanh Cho Bạn
Đây là thứ tự mình thường follow khi optimize một React app:
- Giảm bundle size — code split, lazy load, compress assets. Cái này cho hiệu quả nhanh nhất.
- Tối ưu API calls — cache, debounce, dùng skeleton/content loader trong lúc chờ.
- Virtualize danh sách dài — react-window, chỉ 5 phút setup.
- Fix re-render thừa — React DevTools Profiler + why-did-you-render. Cái này tốn thời gian nhất nhưng đôi khi impact lớn nhất.
- CSS thay JS cho animation — transform, will-change, custom properties.
- SSR/SSG nếu cần — Next.js làm việc này khá smooth, nhưng migration cost cao.
Performance optimization không phải là làm một lần rồi thôi. Mỗi feature mới đều có thể introduce vấn đề mới. Điều quan trọng là bạn có công cụ và mindset để phát hiện sớm. Profile trước, optimize sau, đo lại kết quả. Đừng optimize bằng cảm giác — hãy để số liệu dẫn đường. Ship it fast, but make it fast too. 🚀
Nguyen Nhat Long
@longnnThấy hay? Chia sẻ cho bạn bè!
Bài viết liên quan
Có thể bạn cũng thích

Zero-Downtime Deployment: Deploy mà user không biết
Làm sao để deploy bản mới lên production mà không ai nhận ra? Cùng tìm hiểu các chiến lược Blue-Green, Rolling, Canary và cách áp dụng thực tế.
Tolaria Quản lý knowledge base bằng Markdown như dân chuyên nghiệp
Tolaria là desktop app open source giúp quản lý knowledge base bằng markdown files, git-first, offline-first. Đây có thể là thứ bạn đang thiếu cho second brain.
Floci: Giã từ LocalStack, chào đón AWS emulator miễn phí thực sự
LocalStack Community đã sunset. Floci là alternative mới — mã nguồn mở, không cần auth token, startup 24ms. Đây là lý do bạn nên thử ngay.