Tình huống: Khi chia nhỏ mã nguồn (code-splitting) làm hỏng giao diệnGần đây tôi có kiểm tra một bảng điều khiển (dashboard) nơi gói JavaScript chính đã phình to lên hơn 2.5MB. Để cắt giảm dung lượng, tôi đã sử dụng React.lazy() để chia nhỏ các lưới dữ liệu nặng và biểu đồ D3 thành các phần (chunks) nhỏ hơn, tải theo yêu cầu. Việc phát triển ở môi trường local có vẻ rất mượt mà. Tuy nhiên, ngay khi tôi triển khai lên môi trường staging và nhấp vào tab 'Analytics', toàn bộ ứng dụng biến mất. Thay vào đó là một màn hình trắng xóa và một lỗi lớn trong bảng điều khiển của trình duyệt.
Error: A React component suspended while rendering, but no fallback UI was specified.
Sự cố này xảy ra vì React đã cố gắng render một component chưa thực sự có sẵn. Do tệp JS vẫn đang được tải xuống qua mạng, React đã rơi vào trạng thái "hoảng loạn". Nó không có hướng dẫn nào về việc nên hiển thị gì cho người dùng trong vài trăm mili giây chờ đợi đó.
Tại sao React lại đưa ra lỗi nàyTrong hệ sinh thái React, "suspending" (tạm dừng) là một tín hiệu. Một component về cơ bản sẽ báo cho trình render biết: "Tôi chưa sẵn sàng vì tôi đang đợi một tài nguyên — như phản hồi từ mạng hoặc một phần mã nguồn được tải chậm (lazy-loaded chunk)."
Khi bạn sử dụng React.lazy(), bạn đang tạo ra một lệnh dynamic import. Việc tải tệp đó xuống cần có thời gian, đặc biệt là trên các kết nối 3G. Nếu bạn không bao bọc component lazy đó trong một boundary (ranh giới) <Suspense>, React sẽ từ chối hiển thị giao diện bị thiếu hoặc bị lỗi. Thay vào đó, nó sẽ ném ra một lỗi để ngăn chặn trạng thái không nhất quán.
Bạn có khả năng gặp phải lỗi này khi:
- Triển khai chia nhỏ mã nguồn dựa trên route với
react-router-dom.- Bật cờsuspense: truetrong TanStack Query (React Query).- Sử dụng hookuse()(đang thử nghiệm) để lấy dữ liệu trực tiếp trong quá trình render.## Giải pháp: Triển khai các Suspense BoundaryKhắc phục lỗi này chỉ mất hai phút. Bạn cần bao bọc component gây lỗi trong một thẻ<Suspense>và cung cấp một propfallback. Prop này cho React biết chính xác cần render cái gì trong khi nội dung chính đang tải.
Ví dụ 1: Khắc phục các Route tải chậm (Lazy Loaded Routes)Nếu tệp App.js của bạn trông giống như đoạn mã bên dưới, nó sẽ bị crash ngay khi người dùng điều hướng đến dashboard:
import React, { lazy } from 'react';
const HeavyDashboard = lazy(() => import('./pages/HeavyDashboard'));
function App() {
return (
<div>
<HeavyDashboard /> {/* ❌ Dòng này kích hoạt lỗi */}
</div>
);
}
Để khắc phục, hãy thêm component Suspense và định nghĩa một trạng thái chờ (loading state):
import React, { lazy, Suspense } from 'react';
const HeavyDashboard = lazy(() => import('./pages/HeavyDashboard'));
function App() {
return (
<div>
<Suspense fallback={<div className="spinner">Đang tải Dashboard...</div>}>
<HeavyDashboard />
</Suspense>
</div>
);
}
Ví dụ 2: Fallback chi tiết so với Fallback toàn cụcBạn không cần một bộ bao bọc cho mọi component riêng lẻ. Một boundary ở cấp cao có thể bắt được nhiều component con được tải chậm. Nhưng hãy cẩn thận: nếu bất kỳ component con nào tạm dừng (suspend), toàn bộ khối đó sẽ bị thay thế bởi fallback.
<Suspense fallback={<GlobalLoader />}>
<Header />
<LazySidebar />
<LazyMainContent />
</Suspense>
Trong ví dụ trên, Header sẽ biến mất bất cứ khi nào sidebar đang tải. Để giữ cho thanh điều hướng luôn hiển thị trong khi nội dung đang tải, hãy lồng các boundary của bạn một cách chiến lược hơn:
<Header />
<div className="layout">
<Suspense fallback={<SidebarSkeleton />}>
<LazySidebar />
</Suspense>
<Suspense fallback={<MainSkeleton />}>
<LazyMainContent />
</Suspense>
</div>
Xác minh: Kiểm tra trên mạng chậmMáy cục bộ của bạn có thể quá nhanh để thấy được trạng thái đang tải. Để xác nhận giải pháp hoạt động, hãy mô phỏng môi trường thực tế bằng Chrome DevTools:
- Nhấn
F12và mở tab Network.- Tìm menu thả xuống có ghi "No throttling."- Chọn Slow 3G (điều này mô phỏng độ trễ khoảng 400ms).- Tải lại trang của bạn.- Xác minh rằng màn hình skeleton hoặc spinner của bạn xuất hiện trước khi component hiện ra.## Mẹo chuyên nghiệp cho môi trường Production- Prop fallback là bắt buộc: Ngay cả khi bạn không muốn hiển thị gì, bạn vẫn phải truyềnfallback={null}.- Xử lý lỗi mạng: Suspense chỉ xử lý việc chờ đợi. Nếu người dùng mất kết nối internet và phần mã JS không tải xuống được, Suspense sẽ không giúp được gì. Hãy bao bọc Suspense boundary của bạn trong một ErrorBoundary để bắt các lỗi 404 hoặc hết hạn thời gian mạng (network timeout).- Phân tích Bundle: Sử dụng các công cụ nhưwebpack-bundle-analyzerđể đảm bảo các lệnh importReact.lazy()của bạn thực sự tạo ra các tệp riêng biệt.

