- Giới thiệu về vấn đề tải tài nguyên trong React SPA
- Một số khái niệm cơ bản
- Cách triển khai Lazy Loading với React.lazy và Suspense
- Chiến lược chia nhỏ các chunk: Route-based vs Component-based
- Lazy loading dữ liệu và hình ảnh
- Quản lý fallback UI và trải nghiệm người dùng
- Best practices khi sử dụng React.lazy và Suspense
- So sánh React.lazy với các giải pháp thay thế
- Triển khai thực tế trong dự án lớn
- Kết luận
Giới thiệu về vấn đề tải tài nguyên trong React SPA
Trong các ứng dụng Single Page Application (SPA) được xây dựng bằng React, việc tải toàn bộ bundle JavaScript ngay khi người dùng truy cập trang đầu tiên có thể gây ra thời gian khởi động (initial load) kéo dài, làm giảm trải nghiệm người dùng. Khi bundle chứa hàng trăm megabyte, trình duyệt phải tải, phân tích và thực thi toàn bộ mã trước khi hiển thị bất kỳ nội dung nào. Đặc biệt trên các thiết bị di động hoặc kết nối mạng chậm, vấn đề này trở nên nghiêm trọng.
Lazy loading và code splitting là hai kỹ thuật quan trọng giúp giảm kích thước bundle ban đầu bằng cách tách các phần mã không cần thiết ngay lập tức ra thành các chunk riêng, chỉ tải khi thực sự cần. React cung cấp React.lazy và Suspense để thực hiện lazy loading cho các component, trong khi Webpack (hoặc Vite) hỗ trợ dynamic import() để tạo các chunk.
Một số khái niệm cơ bản
Code Splitting
Code splitting là quá trình chia mã nguồn thành nhiều file (chunk) nhỏ hơn, mỗi chunk được tải độc lập. Khi cấu hình bundler (Webpack, Vite, Rollup) với import() động, bundler sẽ tự động tạo các chunk tương ứng. Điều này cho phép trình duyệt chỉ tải những phần mã mà người dùng thực sự yêu cầu.
Lazy Loading
Lazy loading là việc trì hoãn việc tải một tài nguyên (component, hình ảnh, dữ liệu) cho tới khi nó được yêu cầu. Trong React, React.lazy nhận một hàm trả về import() và trả về một component có thể được render bên trong Suspense. Khi component được render lần đầu, React sẽ bắt đầu tải chunk tương ứng và hiển thị fallback cho tới khi tải xong.
Cách triển khai Lazy Loading với React.lazy và Suspense
Dưới đây là một ví dụ cơ bản về cách lazy load một trang trong một ứng dụng React Router v6. Đầu tiên, chúng ta tạo các component trang riêng biệt:
<!-- Home.jsx -->
import React from 'react';
export default function Home() {
return (
<div>
<h1>Trang Chủ</h1>
<p>Nội dung trang chủ</p>
</div>
);
}
<!-- About.jsx -->
import React from 'react';
export default function About() {
return (
<div>
<h1>Giới thiệu</h1>
<p>Thông tin về dự án</p>
</div>
);
}
Sau đó, trong file định tuyến chính, chúng ta sử dụng React.lazy để tải các component này một cách lười biếng:
import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
// Lazy load các trang
const Home = lazy(() => import('./Home'));
const About = lazy(() => import('./About'));
function App() {
return (
<Router>
<nav>
<Link to="/">Home</Link> | <Link to="/about">About</Link>
</nav>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Suspense>
</Router>
);
}
export default App;Trong ví dụ trên, khi người dùng truy cập /about, React sẽ tải chunk About.jsx chỉ tại thời điểm đó, giảm kích thước bundle ban đầu.
Chiến lược chia nhỏ các chunk: Route-based vs Component-based
Có hai cách tiếp cận phổ biến để quyết định đâu là điểm chia chunk:
- Route-based splitting: Mỗi route (trang) được lazy load riêng. Đây là cách đơn giản và phù hợp với các ứng dụng có cấu trúc trang rõ ràng.
- Component-based splitting: Các component con lớn, không liên quan trực tiếp tới route, cũng được tách ra. Ví dụ: một biểu đồ phức tạp, một editor rich text, hoặc một thư viện UI nặng.
Việc lựa chọn phụ thuộc vào tần suất sử dụng và kích thước của component. Nếu một component chỉ xuất hiện trong một vài trường hợp, nên tách ra để tránh tải thừa.
Lazy loading dữ liệu và hình ảnh
Lazy loading không chỉ áp dụng cho mã JavaScript mà còn cho dữ liệu và tài nguyên tĩnh. Đối với hình ảnh, loading="lazy" trong thẻ <img> là một giải pháp đơn giản, nhưng khi cần tải hình ảnh trong component React, chúng ta có thể kết hợp useEffect và IntersectionObserver để chỉ tải khi phần tử xuất hiện trong viewport.
import React, { useRef, useState, useEffect } from 'react';
function LazyImage({ src, alt, ...rest }) {
const imgRef = useRef();
const [visible, setVisible] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(
entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
setVisible(true);
observer.disconnect();
}
});
},
{ threshold: 0.1 }
);
if (imgRef.current) observer.observe(imgRef.current);
return () => observer.disconnect();
}, []);
return (
<img
ref={imgRef}
src={visible ? src : ''}
alt={alt}
{...rest}
/>
);
}
Đối với dữ liệu, chúng ta có thể kết hợp React Query (hoặc SWR) với Suspense để thực hiện fetch lazy. Khi component được render, dữ liệu sẽ được fetch tự động và Suspense sẽ hiển thị fallback cho tới khi dữ liệu sẵn sàng.
Quản lý fallback UI và trải nghiệm người dùng
Fallback UI là phần nội dung tạm thời hiển thị trong khi chunk đang được tải. Để tránh gây cảm giác “đơ” cho người dùng, chúng ta nên:
- Thiết kế skeleton screen (khung khung) thay vì chỉ hiển thị
Loading...chung. - Giữ trạng thái UI nhất quán: nếu một trang có nhiều component lazy, nên bao bọc chúng trong một
Suspensechung để tránh hiển thị nhiều fallback chồng lên nhau. - Sử dụng
useTransition(React 18) để tách các cập nhật UI quan trọng và không quan trọng, giảm thiểu “jank”.
Ví dụ về skeleton screen đơn giản:
import React from 'react';
import './Skeleton.css'; // Giả sử có CSS cho .skeleton
function CardSkeleton() {
return (
<div className="card skeleton">
<div className="avatar"></div>
<div className="line short"></div>
<div className="line"></div>
<div className="line"></div>
</div>
);
}
Best practices khi sử dụng React.lazy và Suspense
- Không lazy load component quá nhỏ: Việc tạo chunk cho các component chỉ vài kilobyte sẽ gây overhead tải mạng và parsing không đáng kể.
- Đặt fallback ở mức phù hợp: Nếu chỉ một phần nhỏ của trang được lazy, fallback nên là phần UI tương ứng, không phải toàn bộ trang.
- Kiểm tra bundle size: Sử dụng
webpack-bundle-analyzerhoặcvite build --mode analysisđể xác nhận các chunk được tạo như mong muốn. - Cache các chunk: Đảm bảo cấu hình server trả về header
Cache-Controlhợp lý để các chunk được lưu trong cache trình duyệt. - Preload quan trọng: Đối với các chunk mà người dùng thường truy cập ngay sau trang chủ (ví dụ: dashboard), sử dụng
<link rel="preload" as="script" href="/static/js/2.chunk.js">để tải trước.
So sánh React.lazy với các giải pháp thay thế
Mặc dù React.lazy là giải pháp tích hợp sẵn, một số dự án vẫn chọn các thư viện như loadable-components hoặc react-loadable để có khả năng server‑side rendering (SSR) hoặc kiểm soát chi tiết hơn về prefetching.
| Tiêu chí | React.lazy + Suspense | loadable-components |
|---|---|---|
| SSR hỗ trợ | Không (cần React 18 experimental) | Có (hỗ trợ đầy đủ) |
| Prefetch / Preload | Giới hạn (cần link thủ công) |
Có API loadableReady và loadable.preload() |
| Độ phức tạp cấu hình | Rất đơn giản | Phức tạp hơn, cần Babel plugin |
Với các dự án SPA không cần SSR, React.lazy là lựa chọn tối ưu vì tính đơn giản và tích hợp sẵn trong React 16.6 trở lên.
Triển khai thực tế trong dự án lớn
Giả sử chúng ta có một dashboard quản trị với các module: Analytics, User Management, Settings. Mỗi module có kích thước lớn (hơn 200KB nén). Để giảm thời gian tải trang đầu, chúng ta:
- Áp dụng route‑based splitting cho các route
/analytics,/users,/settingsbằngReact.lazy. - Trong module Analytics, tách các biểu đồ (Chart.js) và bảng dữ liệu (DataTable) thành các component lazy riêng, vì người dùng có thể chỉ xem một trong số chúng.
- Sử dụng
useTransitionđể chuyển đổi route mà không block UI, đồng thời hiển thị skeleton cho nội dung chính. - Cấu hình Webpack
splitChunksđể tách các thư viện chung (React, React‑DOM, lodash) thành chunkvendorsvà đặtcacheGroupcho các chunk chung.
Ví dụ cấu hình Webpack:
module.exports = {
// ...
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
reactVendor: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'react-vendor',
priority: 20,
enforce: true,
},
utilityVendor: {
test: /[\\/]node_modules[\\/](lodash|date-fns)[\\/]/,
name: 'utility-vendor',
priority: 10,
},
},
},
},
};Sau khi triển khai, chúng ta đo lường bằng Lighthouse và nhận được giảm thời gian First Contentful Paint (FCP) từ 3.2s xuống còn 1.8s, đồng thời Time to Interactive (TTI) giảm 30%.
Kết luận
Lazy loading và code splitting là những công cụ mạnh mẽ giúp tối ưu hiệu năng cho các ứng dụng React SPA. Bằng cách xác định đúng điểm chia chunk, sử dụng React.lazy kết hợp Suspense, thiết kế fallback UI hợp lý, và cấu hình bundler để tách các thư viện chung, chúng ta có thể giảm đáng kể thời gian tải ban đầu, cải thiện trải nghiệm người dùng và giảm tải băng thông. Đối với những dự án cần SSR hoặc prefetch nâng cao, các thư viện như loadable-components là lựa chọn thay thế phù hợp. Nếu bạn muốn nắm vững cách xây dựng ứng dụng React hiện đại, bao gồm việc tích hợp TypeScript, quản lý state, và tối ưu performance, Tham khảo khóa học "Lập trình Front-End với ReactJS + TypeScript" tại đây.







