Quay lại
Frontend
5 phút đọc4 tháng 4, 20266

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.

N

Nguyen Nhat Long

@longnn

React App Chậm? Đây Là Cách Mình Xử Lý Sau 5 Năm Đau Khổ

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

Minh họa cơ chế re-render trong React

Đâ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:

JSX
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:

JSX
1const cardStyle = { marginTop: 10 };
2const defaultFilters = ["active", "premium"];
3
4function 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:

JSX
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:

JSX
1// ❌ Gây re-render mỗi lần interval chạy
2const [count, setCount] = useState(0);
3
4// ✅ Không gây re-render
5const countRef = useRef(0);

Memo hóa component

  • Function component: wrap bằng React.memo()
  • Class component: extend PureComponent
  • Dùng useMemo cho computed values, useCallback cho 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.

Minh họa so sánh prop drilling và context

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

JSX
1const HeavyChart = React.lazy(() => import('./HeavyChart'));
2
3function 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:

Minh họa virtualization cho danh sách dài
JSX
1import { FixedSizeList } from 'react-window';
2
3<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 transform thay 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: transform cho 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:

JSX
1const [showChart, setShowChart] = useState(false);
2
3useEffect(() => {
4 requestAnimationFrame(() => setShowChart(true));
5}, []);
6
7return 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:

  1. Giảm bundle size — code split, lazy load, compress assets. Cái này cho hiệu quả nhanh nhất.
  2. Tối ưu API calls — cache, debounce, dùng skeleton/content loader trong lúc chờ.
  3. Virtualize danh sách dài — react-window, chỉ 5 phút setup.
  4. 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.
  5. CSS thay JS cho animation — transform, will-change, custom properties.
  6. 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. 🚀

NN

Nguyen Nhat Long

@longnn

Thấy hay? Chia sẻ cho bạn bè!

Bài viết liên quan

Có thể bạn cũng thích

Xem tất cả