Bài 19.4: Code splitting với React.lazy

Bài 19.4: Code splitting với React.lazy
Chào mừng bạn quay trở lại với hành trình khám phá thế giới lập trình web cùng FullhouseDev!
Ở các bài trước, chúng ta đã xây dựng các component React và kết hợp chúng lại để tạo nên giao diện người dùng. Tuy nhiên, khi ứng dụng của bạn ngày càng lớn, số lượng component, thư viện và logic xử lý tăng lên, kích thước của file JavaScript cuối cùng được tải xuống trình duyệt cũng tăng lên đáng kể. Điều này có thể khiến thời gian tải trang ban đầu trở nên chậm chạp, ảnh hưởng nghiêm trọng đến trải nghiệm người dùng và thậm chí là thứ hạng SEO.
Làm thế nào để giải quyết vấn đề này? Code Splitting (Chia nhỏ mã nguồn) chính là câu trả lời! Đây là một kỹ thuật giúp chia nhỏ gói mã JavaScript lớn thành nhiều gói nhỏ hơn ("chunks"). Khi người dùng truy cập vào ứng dụng, trình duyệt chỉ cần tải xuống những gói mã cần thiết cho phần giao diện họ đang xem, thay vì tải toàn bộ ứng dụng cùng một lúc. Các phần còn lại sẽ được tải theo yêu cầu khi cần.
Và React cung cấp cho chúng ta một công cụ tuyệt vời để thực hiện Code Splitting một cách dễ dàng và hiệu quả: đó chính là React.lazy
kết hợp với React.Suspense
.
Tại sao Code Splitting lại quan trọng?
Hãy tưởng tượng ứng dụng web của bạn như một cửa hàng bách hóa tổng hợp. Khi một khách hàng chỉ muốn mua một gói kẹo (tức là chỉ xem một trang cụ thể), bạn không bắt họ phải mang toàn bộ cửa hàng về nhà. Thay vào đó, bạn chỉ đưa cho họ gói kẹo họ cần. Code Splitting làm điều tương tự với mã nguồn của bạn.
Lợi ích chính của Code Splitting bao gồm:
- Tải trang ban đầu nhanh hơn: Chỉ những đoạn mã cần thiết cho giao diện ban đầu mới được tải, giảm thời gian chờ đợi cho người dùng.
- Giảm thiểu việc sử dụng tài nguyên: Trình duyệt chỉ cần xử lý và thực thi lượng mã ít hơn lúc ban đầu.
- Cải thiện trải nghiệm người dùng: Ứng dụng cảm thấy "nhẹ" và phản hồi nhanh hơn.
- Tối ưu băng thông: Đặc biệt quan trọng với người dùng sử dụng mạng chậm hoặc dữ liệu di động.
Trước đây, để làm Code Splitting trong React, chúng ta thường phải dùng các thư viện của bên thứ ba hoặc cấu hình Webpack phức tạp. Nhưng với React.lazy
, mọi thứ đã trở nên đơn giản hơn bao giờ hết!
React.lazy
: Load component theo yêu cầu
React.lazy
là một hàm cho phép bạn render một import động (dynamic import) như một component thông thường.
Cú pháp rất đơn giản:
const TenComponentCuaBan = React.lazy(() => import('./duongDanDenComponent'));
React.lazy
nhận một đối số là một hàm trả về một Promise
. Promise
này sẽ phân giải (resolve) thành một module có thuộc tính default export
là component React mà bạn muốn tải.
Ví dụ cơ bản:
Giả sử bạn có một component tên là HeavyComponent
ở trong file HeavyComponent.js
. Component này rất lớn hoặc chứa logic phức tạp mà không phải lúc nào cũng cần thiết.
Thay vì import thông thường:
// import thông thường (sẽ được đóng gói vào bundle chính)
import HeavyComponent from './HeavyComponent';
function App() {
return (
<div>
<h1>Ứng dụng chính</h1>
{/* HeavyComponent luôn được load dù có hiển thị hay không */}
<HeavyComponent />
</div>
);
}
Chúng ta sẽ sử dụng React.lazy
:
// import động với React.lazy
const HeavyComponent = React.lazy(() => import('./HeavyComponent'));
function App() {
return (
<div>
<h1>Ứng dụng chính</h1>
{/* HeavyComponent sẽ chỉ được load khi nó được render */}
<HeavyComponent />
</div>
);
}
Giải thích code:
const HeavyComponent = React.lazy(() => import('./HeavyComponent'));
: Dòng này không tải ngay lập tức fileHeavyComponent.js
. Thay vào đó, nó tạo ra một "component lười" (lazy component
). Khi React lần đầu tiên cố gắng render<HeavyComponent />
, hàmimport('./HeavyComponent')
mới được gọi. Hàmimport()
này là cú pháp chuẩn của JavaScript (ES dynamic imports) và sẽ báo cho bundler (như Webpack hoặc Parcel) biết rằng đây là một điểm chia tách mã. Bundler sẽ tạo ra một file JavaScript riêng choHeavyComponent
và các dependencies của nó.
Tuy nhiên, có một vấn đề nhỏ. Khi HeavyComponent
đang được tải, React cần biết phải hiển thị cái gì trong lúc chờ đợi. Đây chính là lúc React.Suspense
phát huy tác dụng!
React.Suspense
: Xử lý trạng thái chờ đợi
React.lazy
phải được sử dụng bên trong React.Suspense
. Suspense
là một component của React cho phép bạn "đình chỉ" việc render một phần của cây component và hiển thị một giao diện dự phòng (fallback) trong khi đợi điều kiện (trong trường hợp này là tải mã) hoàn thành.
Suspense
nhận một prop bắt buộc là fallback
. Prop này chứa bất kỳ phần tử React nào bạn muốn hiển thị trong khi các component "lười" bên trong nó đang được tải.
Kết hợp React.lazy
và React.Suspense
:
import React, { Suspense } from 'react';
// import động với React.lazy
const HeavyComponent = React.lazy(() => import('./HeavyComponent'));
function App() {
return (
<div>
<h1>Ứng dụng chính</h1>
{/* Sử dụng Suspense để bọc component lười */}
<Suspense fallback={<div>Đang tải HeavyComponent...</div>}>
{/* Component lười */}
<HeavyComponent />
</Suspense>
</div>
);
}
Giải thích code:
import React, { Suspense } from 'react';
: Chúng ta cần importSuspense
từ thư việnreact
.<Suspense fallback={<div>Đang tải HeavyComponent...</div>}>
: Đây là boundary củaSuspense
. Bất kỳ component con nào bên trongSuspense
(bao gồm cảHeavyComponent
) mà "đình chỉ" việc render (ví dụ: do đang chờ mã được tải) sẽ khiếnSuspense
hiển thị nội dung của propfallback
(<div>Đang tải HeavyComponent...</div>
).- Khi
HeavyComponent
được tải xong,Suspense
sẽ tự động chuyển sang hiển thịHeavyComponent
thực tế.
Bạn có thể đặt nhiều component lazy
bên trong cùng một boundary Suspense
.
import React, { Suspense } from 'react';
const ComponentA = React.lazy(() => import('./ComponentA'));
const ComponentB = React.lazy(() => import('./ComponentB'));
function App() {
return (
<div>
<h1>Ứng dụng chính</h1>
<Suspense fallback={<div>Đang tải các component...</div>}>
<ComponentA />
<ComponentB /> {/* Nếu ComponentA hoặc ComponentB đang tải, fallback sẽ hiển thị */}
</Suspense>
</div>
);
}
Nếu cả ComponentA
và ComponentB
đều là component lười và được render lần đầu tiên cùng lúc, React sẽ cố gắng tải cả hai file mã nguồn cùng lúc. Trong khi cả hai chưa sẵn sàng, fallback của Suspense
sẽ được hiển thị.
Các trường hợp sử dụng phổ biến
Code Splitting đặc biệt hiệu quả trong một số trường hợp cụ thể:
1. Chia nhỏ theo Route (Route-based splitting)
Đây là cách phổ biến và hiệu quả nhất để áp dụng Code Splitting. Thông thường, người dùng chỉ xem một hoặc vài trang (route) của ứng dụng tại một thời điểm. Chúng ta có thể chia nhỏ mã nguồn sao cho mỗi route có gói mã riêng. Khi người dùng điều hướng đến một route, chỉ mã nguồn cần thiết cho route đó mới được tải.
Sử dụng với một thư viện định tuyến như react-router-dom
:
import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
// Import các component route một cách lười biếng
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
function App() {
return (
<Router>
<nav>
<ul>
<li><Link to="/">Trang chủ</Link></li>
<li><Link to="/about">Giới thiệu</Link></li>
<li><Link to="/dashboard">Dashboard</Link></li>
</ul>
</nav>
{/* Suspense bọc toàn bộ Routes để xử lý trạng thái tải của các route lười */}
<Suspense fallback={<div>Đang tải trang...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Suspense>
</Router>
);
}
export default App;
Giải thích code:
const Home = lazy(() => import('./pages/Home'));
: ComponentHome
được import lười. Tương tự vớiAbout
vàDashboard
.<Suspense fallback={<div>Đang tải trang...</div>}>
: Một boundarySuspense
được đặt bên ngoài<Routes>
. Bất cứ khi nàoreact-router-dom
cố gắng render mộtRoute
mà component của route đó đang được tải (do nó là component lười),Suspense
sẽ hiển thị fallback ("Đang tải trang...").- Khi người dùng click vào link,
react-router-dom
sẽ chuyển route, React sẽ cố gắng render component tương ứng. Nếu component đó làlazy
và chưa được tải, nó sẽ "đình chỉ", và fallback củaSuspense
sẽ xuất hiện cho đến khi mã nguồn được tải xong.
Cách này giúp giảm đáng kể kích thước bundle ban đầu vì mã nguồn của About
và Dashboard
chỉ được tải khi người dùng truy cập vào các trang đó.
2. Chia nhỏ theo Component (Component-based splitting)
Bạn cũng có thể áp dụng Code Splitting cho các component không phải là route, đặc biệt là những component chỉ hiển thị trong một số điều kiện nhất định (ví dụ: modal, popup, phần nội dung mở rộng khi click nút).
Ví dụ: Tải modal chỉ khi người dùng click vào nút mở modal
import React, { useState, Suspense, lazy } from 'react';
// Component Modal được import lười
const LazyModal = lazy(() => import('./components/Modal'));
function App() {
const [showModal, setShowModal] = useState(false);
const handleOpenModal = () => {
setShowModal(true);
};
const handleCloseModal = () => {
setShowModal(false);
};
return (
<div>
<h1>Ứng dụng</h1>
<button onClick={handleOpenModal}>Mở Modal</button>
{showModal && (
{/* Chỉ render Suspense và LazyModal khi showModal là true */}
<Suspense fallback={<div>Đang tải Modal...</div>}>
<LazyModal onClose={handleCloseModal} />
</Suspense>
)}
</div>
);
}
export default App;
Giải thích code:
const LazyModal = lazy(() => import('./components/Modal'));
: Định nghĩaLazyModal
là một component lười. Mã nguồn củaModal.js
sẽ không được tải ngay.const [showModal, setShowModal] = useState(false);
: Sử dụng state để kiểm soát việc hiển thị modal.handleOpenModal
vàhandleCloseModal
: Hàm để cập nhật stateshowModal
.{showModal && (...) }
: Đây là kỹ thuật render có điều kiện. Chỉ khishowModal
làtrue
, phần code bên trong&&
mới được thực thi.<Suspense fallback={<div>Đang tải Modal...</div>}>
: BoundarySuspense
chỉ được render khishowModal
làtrue
.- Khi người dùng click nút "Mở Modal",
setShowModal(true)
được gọi. React re-render và thấyshowModal
làtrue
, nên nó cố gắng render<Suspense>
và<LazyModal>
. VìLazyModal
là component lười và có thể chưa được tải, nó "đình chỉ", vàSuspense
hiển thị fallback. KhiModal.js
và các dependencies của nó tải xong,Suspense
sẽ hiển thịLazyModal
thực tế.
Cách này rất hiệu quả cho các thành phần giao diện lớn, phức tạp, hoặc ít được sử dụng như modal, popover, trình soạn thảo rich text, v.v. Mã nguồn của chúng sẽ chỉ được tải khi người dùng thực sự cần đến.
Hiển thị trạng thái tải (Loading States)
Prop fallback
của Suspense
có thể là bất kỳ element React nào. Bạn có thể hiển thị một dòng chữ đơn giản, một spinner loading, hoặc thậm chí là một skeleton screen (phiên bản đơn giản hóa của giao diện cuối cùng) để cải thiện trải nghiệm người dùng trong lúc chờ đợi.
Ví dụ với Spinner Component:
Giả sử bạn có một component Spinner
đơn giản:
// components/Spinner.js
import React from 'react';
function Spinner() {
return (
<div className="spinner">
{/* CSS để tạo hình ảnh spinner */}
Loading...
</div>
);
}
export default Spinner;
Bạn có thể sử dụng nó làm fallback:
import React, { Suspense, lazy } from 'react';
import Spinner from './components/Spinner'; // Import component Spinner
const PageContent = lazy(() => import('./components/PageContent'));
function MyPage() {
return (
<div>
<h2>Nội dung trang</h2>
<Suspense fallback={<Spinner />}> {/* Sử dụng component Spinner làm fallback */}
<PageContent />
</Suspense>
</div>
);
}
Giải thích code:
- Thay vì sử dụng một
div
đơn giản, chúng ta import và sử dụng componentSpinner
làm giá trị cho propfallback
. Điều này giúp giao diện chờ đợi trở nên chuyên nghiệp và hấp dẫn hơn.
Xử lý lỗi tải (Error Handling)
Mặc dù Suspense
xử lý trạng thái tải, nó không xử lý các lỗi xảy ra trong quá trình tải mã nguồn (ví dụ: lỗi mạng, file không tồn tại). Nếu xảy ra lỗi, ứng dụng của bạn có thể bị crash.
Để xử lý các lỗi tải này, bạn cần sử dụng Error Boundaries. Error Boundaries là các component React class sử dụng các lifecycle method như static getDerivedStateFromError()
hoặc componentDidCatch()
để bắt lỗi JavaScript ở bất kỳ đâu trong cây component con của chúng.
Bạn có thể bọc boundary Suspense
của mình trong một Error Boundary để xử lý cả trạng thái tải và lỗi tải.
Ví dụ khái niệm (sử dụng Error Boundary cơ bản):
import React, { useState, Suspense, lazy } from 'react';
// Giả sử bạn đã tạo ErrorBoundary component
// Xem thêm về Error Boundaries trong tài liệu React
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// Cập nhật state để lần render tiếp theo sẽ hiển thị giao diện fallback.
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// Bạn cũng có thể log lỗi đến một dịch vụ báo cáo lỗi
console.error("Lỗi tải component:", error, errorInfo);
}
render() {
if (this.state.hasError) {
// Bạn có thể render bất kỳ giao diện fallback tùy chỉnh nào
return <h1>Đã xảy ra lỗi khi tải thành phần này.</h1>;
}
return this.props.children;
}
}
// Component lười
const MyProblematicComponent = lazy(() => import('./components/MyProblematicComponent'));
function App() {
return (
<div>
<h1>Ứng dụng</h1>
{/* Bọc Suspense trong ErrorBoundary */}
<ErrorBoundary>
<Suspense fallback={<div>Đang tải component...</div>}>
<MyProblematicComponent />
</Suspense>
</ErrorBoundary>
</div>
);
}
export default App;
Giải thích code:
ErrorBoundary
là một component class cơ bản bắt lỗi từ cây con.<ErrorBoundary>
được đặt ở lớp bên ngoài<Suspense>
.- Nếu
MyProblematicComponent
gặp lỗi trong quá trình import (ví dụ: file không tìm thấy), Error Boundary sẽ bắt được lỗi đó và hiển thị giao diện fallback của nó (<h1>Đã xảy ra lỗi...</h1>
). - Điều này đảm bảo rằng ứng dụng của bạn không bị crash hoàn toàn chỉ vì một phần nhỏ không tải được.
Trong thực tế, bạn nên sử dụng các thư viện Error Boundary đã được phát triển tốt như react-error-boundary
để đơn giản hóa việc này.
Bundler hỗ trợ
Điều quan trọng cần lưu ý là React.lazy
và import()
động hoạt động được là nhờ sự hỗ trợ từ các công cụ đóng gói module (bundler) như Webpack, Parcel, hoặc Rollup. Các bundler hiện đại sẽ tự động nhận diện cú pháp import()
động và tạo ra các file mã nguồn (chunks) riêng biệt cho các module được import theo cách này.
Khi bạn build ứng dụng cho production, bundler sẽ phân tích các điểm import()
động và xuất ra nhiều file .js
nhỏ thay vì một file .js
khổng lồ duy nhất.
Một vài lưu ý khi sử dụng React.lazy và Suspense
- Không sử dụng với Server-Side Rendering (SSR): Hiện tại,
React.lazy
vàSuspense
chủ yếu được thiết kế cho client-side rendering. Để sử dụng Code Splitting với SSR, bạn cần các giải pháp hoặc thư viện bổ sung (như Loadable Components hoặc cấu hình phức tạp hơn với Next.js/Gatsby). React đang phát triển các tính năng SSR tương thích với Suspense trong tương lai. - Vị trí của Suspense: Bạn có thể đặt boundary
Suspense
ở bất kỳ đâu trong cây component. Đặt nó càng cao càng tốt trong phần mà bạn muốn xử lý tải chung (ví dụ: bọc toàn bộ phần nội dung của route) để hiển thị fallback cho nhiều component lười cùng lúc. Đặt nó ở mức thấp hơn (gần component lười) nếu bạn muốn fallback chỉ ảnh hưởng đến một phần nhỏ của giao diện. - Đừng lạm dụng: Mặc dù Code Splitting rất hữu ích, việc chia nhỏ quá mức (quá nhiều file nhỏ) cũng có thể gây ra overhead do trình duyệt phải tạo nhiều kết nối HTTP hơn để tải các chunk. Hãy tập trung vào chia nhỏ các phần lớn, không cần thiết ngay lập tức của ứng dụng.
Comments