Bài 23.4: Code splitting và lazy loading

Bài 23.4: Code splitting và lazy loading
Chào mừng bạn quay trở lại với hành trình khám phá Front-end! Trong các bài học trước, chúng ta đã xây dựng nên những ứng dụng web ngày càng phức tạp và mạnh mẽ hơn. Tuy nhiên, khi ứng dụng của bạn phát triển, lượng code JavaScript bạn phải tải về cho người dùng cũng tăng lên đáng kể. Một bundle JavaScript khổng lồ có thể làm chậm đáng kể thời gian tải trang ban đầu, gây ra trải nghiệm không tốt cho người dùng, đặc biệt là trên các thiết bị di động hoặc mạng yếu.
Đây chính là lúc Code Splitting và Lazy Loading tỏa sáng! Chúng là những kỹ thuật cực kỳ quan trọng trong việc tối ưu hiệu suất của các ứng dụng web hiện đại (đặc biệt là Single Page Applications - SPA được xây dựng bằng các framework như React, Angular, Vue).
Code Splitting là gì?
Đơn giản mà nói, Code Splitting là kỹ thuật chia nhỏ file JavaScript lớn của ứng dụng thành các file nhỏ hơn, gọi là chunks. Thay vì tải toàn bộ code ứng dụng trong một lần, trình duyệt chỉ cần tải các chunk cần thiết cho phần hiển thị ban đầu của trang. Các chunk còn lại sẽ được tải theo yêu cầu khi người dùng tương tác hoặc điều hướng đến các phần khác của ứng dụng.
Các công cụ build hiện đại như Webpack, Rollup hay Parcel đều hỗ trợ Code Splitting một cách tự động hoặc thông qua cấu hình.
Tại sao lại cần Code Splitting?
- Tăng tốc độ tải trang ban đầu: Người dùng không phải chờ tải xuống toàn bộ code ứng dụng, chỉ những gì họ thấy ngay lập tức.
- Giảm thiểu việc sử dụng bộ nhớ: Chỉ tải và xử lý code khi cần, giảm áp lực lên bộ nhớ trình duyệt.
- Tối ưu việc sử dụng băng thông: Chỉ tải dữ liệu cần thiết.
Cơ chế hoạt động: Dynamic Imports (import()
)
Trong JavaScript hiện đại, cơ chế chính để thực hiện Code Splitting theo yêu cầu là sử dụng cú pháp import()
động. Không giống như import ... from ...
tĩnh được xử lý khi build, import()
động trả về một Promise và cho phép bạn tải module một cách bất đồng bộ.
Ví dụ đơn giản với JavaScript thuần:
Hãy tưởng tượng bạn có một file JavaScript chứa một chức năng nặng chỉ được sử dụng khi người dùng nhấn vào một nút.
// heavyModule.js
console.log('heavyModule đang được định nghĩa...'); // Log này sẽ xuất hiện khi module được tải
export function performHeavyTask() {
console.log('Performing a heavy task...');
// ... imagine complex calculations or loading a large library here
}
export function anotherFunction() {
console.log('Another function in heavy module.');
}
// main.js (file bundle chính)
console.log('main.js đang chạy...');
const loadButton = document.getElementById('loadHeavyModuleButton');
loadButton.addEventListener('click', async () => {
console.log('Button clicked, trying to load heavy module...');
try {
// Sử dụng dynamic import() để tải module này khi nút được click
const heavyModule = await import('./heavyModule.js');
// Module đã tải thành công, giờ có thể sử dụng các exports từ nó
heavyModule.performHeavyTask();
heavyModule.anotherFunction();
console.log('Heavy module loaded and functions called.');
} catch (error) {
console.error('Failed to load heavy module:', error);
}
});
console.log('main.js đã kết thúc định nghĩa.');
Trong ví dụ trên:
heavyModule.js
sẽ được Webpack (hoặc công cụ build của bạn) tách ra thành một chunk riêng.- Đoạn code trong
main.js
bên ngoàiaddEventListener
sẽ chạy ngay khi trang được tải. - Tuy nhiên, code bên trong callback của
addEventListener
chỉ chạy khi người dùng nhấn nút. Khi đó,import('./heavyModule.js')
sẽ được thực thi. - Trình duyệt sẽ gửi yêu cầu tải về chunk chứa
heavyModule.js
. - Chỉ sau khi chunk này được tải về và phân tích cú pháp thành công (thông qua
await
), bạn mới có thể truy cập các hàm nhưperformHeavyTask
. - Console log "heavyModule đang được định nghĩa..." sẽ chỉ xuất hiện sau khi bạn nhấn nút.
Đây chính là Code Splitting kết hợp Lazy Loading ở cấp độ JavaScript thuần!
Lazy Loading là gì?
Lazy Loading (tải lười) là một chiến lược triển khai dựa trên Code Splitting. Nó tập trung vào việc chỉ tải các phần code, tài nguyên hoặc dữ liệu khi chúng thực sự cần thiết hoặc sắp sửa cần thiết.
Code Splitting là cơ chế kỹ thuật để chia nhỏ code, còn Lazy Loading là nguyên tắc hoặc lý do để sử dụng Code Splitting. Bạn sử dụng Code Splitting để thực hiện Lazy Loading.
Các trường hợp phổ biến để áp dụng Lazy Loading:
- Components hoặc Routes: Tải code của một component hoặc một trang chỉ khi người dùng điều hướng đến trang đó hoặc khi component đó được hiển thị trên màn hình.
- Libraries: Tải các thư viện lớn chỉ khi chức năng sử dụng thư viện đó được kích hoạt lần đầu.
- Media (Images, Videos): Tải ảnh hoặc video chỉ khi chúng cuộn vào khung nhìn của người dùng.
Lazy Loading trong React (với React.lazy
và Suspense
)
React cung cấp các API tích hợp sẵn để dễ dàng thực hiện Lazy Loading các component, tận dụng Code Splitting được xử lý bởi các công cụ build như Webpack. Đó chính là React.lazy
và Suspense
.
React.lazy()
: Là một hàm cho phép bạn định nghĩa một component được tải động. Nó nhận một hàm làm đối số, hàm này sẽ gọiimport()
động để tải component.<Suspense>
: Là một component cho phép bạn "chờ" một component được tải động (và các tài nguyên bất đồng bộ khác trong tương lai). Bạn có thể định nghĩa mộtfallback
(nội dung dự phòng), thường là một spinner tải hoặc thông báo, sẽ hiển thị trong khi component đang được tải.
Ví dụ với React:
Hãy tạo một component đơn giản mà chúng ta muốn tải lười.
// HeavyComponent.js
import React from 'react';
console.log('HeavyComponent đang được định nghĩa...'); // Log này sẽ xuất hiện khi component được tải
function HeavyComponent() {
// Imagine this component contains a lot of UI elements or uses a large library
return (
<div style={{ border: '1px solid blue', padding: '20px', marginTop: '20px' }}>
<h2>Đây là Component Nặng (được Lazy Load)</h2>
<p>Component này chỉ tải khi bạn nhấn nút.</p>
</div>
);
}
export default HeavyComponent;
Bây giờ, sử dụng nó trong ứng dụng chính với React.lazy
và Suspense
.
// App.js
import React, { Suspense, useState } from 'react';
import './App.css'; // assume some basic styling
console.log('App.js đang chạy...');
// Sử dụng React.lazy để định nghĩa component sẽ được tải động
// Hàm này sẽ gọi import() tới HeavyComponent.js
const LazyHeavyComponent = React.lazy(() => import('./HeavyComponent'));
function App() {
const [showHeavy, setShowHeavy] = useState(false);
return (
<div className="App">
<header className="App-header">
<h1>Ứng dụng chính (Initial Bundle)</h1>
<button onClick={() => setShowHeavy(true)}>
Hiển thị Component Nặng
</button>
{/* Sử dụng Suspense để bao bọc component được lazy load */}
{/* fallback hiển thị trong khi component đang được tải */}
<Suspense fallback={<div>Đang tải component nặng...</div>}>
{/* Chỉ render component khi state showHeavy là true */}
{showHeavy && <LazyHeavyComponent />}
</Suspense>
</header>
</div>
);
}
export default App;
Giải thích ví dụ React:
- Khi ứng dụng
App
được tải lần đầu,HeavyComponent.js
không nằm trong bundle JavaScript ban đầu. Console log "HeavyComponent đang được định nghĩa..." sẽ không xuất hiện. LazyHeavyComponent
là một component đặc biệt trả về bởiReact.lazy
.<Suspense fallback={...}>
được đặt xung quanh nơiLazyHeavyComponent
được sử dụng. Nó "bắt" các component con đang trong trạng thái tải (pending) và hiển thị nội dung trongfallback
cho đến khi chúng sẵn sàng.- Ban đầu,
showHeavy
làfalse
, nênLazyHeavyComponent
không được render. - Khi bạn nhấn nút "Hiển thị Component Nặng",
setShowHeavy(true)
được gọi. - React cố gắng render
LazyHeavyComponent
. Lúc này,React.lazy
gọiimport('./HeavyComponent')
. - Trình duyệt bắt đầu tải chunk chứa
HeavyComponent.js
. - Trong lúc tải,
<Suspense>
hiển thịfallback
: "Đang tải component nặng...". - Khi chunk được tải xong, component
HeavyComponent
thực sự được định nghĩa và sẵn sàng. Console log "HeavyComponent đang được định nghĩa..." sẽ xuất hiện trong console. - React render lại, và lần này
LazyHeavyComponent
(giờ đã sẵn sàng) được hiển thị thay vì fallback.
Đây là cách phổ biến nhất để thực hiện Lazy Loading theo component trong React, và nó hoạt động liền mạch với Code Splitting của các công cụ build.
Áp dụng Lazy Loading cho Routes
Một trong những ứng dụng hiệu quả nhất của Lazy Loading là áp dụng cho các route (trang) khác nhau trong ứng dụng SPA. Thay vì tải code cho tất cả các trang khi ứng dụng khởi động, bạn chỉ tải code cho trang hiện tại và tải lười code cho các trang còn lại.
Với React Router, bạn có thể dễ dàng kết hợp React.lazy
và Suspense
để thực hiện điều này:
// App.js (Sử dụng React Router)
import React, { Suspense } from 'react';
import { BrowserRouter as Router, Route, Switch, Link } from 'react-router-dom';
console.log('App.js for Routing đang chạy...');
// Định nghĩa các component cho từng trang sử dụng React.lazy
const HomePage = React.lazy(() => import('./pages/HomePage'));
const AboutPage = React.lazy(() => import('./pages/AboutPage'));
const DashboardPage = React.lazy(() => import('./pages/DashboardPage'));
function App() {
return (
<Router>
<div>
<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>
{/* Sử dụng Suspense để bao bọc các Route */}
{/* Fallback sẽ hiển thị khi chuyển trang và component của trang mới đang tải */}
<Suspense fallback={<div>Đang tải trang...</div>}>
<Switch>
<Route path="/about"><AboutPage /></Route>
<Route path="/dashboard"><DashboardPage /></Route>
{/* Route chính (exact path) nên đặt cuối */}
<Route path="/"><HomePage /></Route>
</Switch>
</Suspense>
</div>
</Router>
);
}
export default App;
Với cấu trúc này:
- Khi người dùng truy cập
/
, chỉ code củaApp.js
vàHomePage.js
(cùng các dependencies của chúng) được tải ban đầu. - Khi người dùng nhấn vào liên kết "Giới Thiệu", React Router sẽ cố gắng render
<AboutPage />
. - Do
<AboutPage />
được định nghĩa bằngReact.lazy
,import('./pages/AboutPage')
được gọi. <Suspense>
bao bọc các route sẽ hiển thị "Đang tải trang..." trong khi chunk củaAboutPage.js
được tải về.- Sau khi tải xong,
AboutPage
hiển thị. Code củaDashboardPage.js
vẫn chưa được tải.
Đây là một mẫu thiết kế rất hiệu quả để giảm thiểu kích thước bundle ban đầu cho các ứng dụng SPA lớn.
Lưu ý quan trọng khi sử dụng Lazy Loading
- Trạng thái Loading: Luôn cung cấp một trạng thái tải (
fallback
trongSuspense
) để người dùng biết điều gì đang xảy ra. Một màn hình trống trong khi code đang tải có thể gây nhầm lẫn. - Xử lý lỗi:
Suspense
hiện tại không xử lý lỗi tải code (ví dụ: lỗi mạng). Bạn cần kết hợp nó với Error Boundaries để hiển thị thông báo lỗi thân thiện cho người dùng nếu việc tải code thất bại. - Server-Side Rendering (SSR):
React.lazy
vàSuspense
được thiết kế chủ yếu cho client-side rendering. Việc sử dụng chúng trong ứng dụng SSR cần cấu hình bổ sung hoặc sử dụng các thư viện hỗ trợ nhưloadable-components
để đảm bảo code được tải và render đúng trên server cũng như client (hydration). - Preloading/Prefetching: Đối với các trang hoặc component mà bạn dự đoán người dùng có thể truy cập sớm (ví dụ: component của trang đăng nhập sau trang chủ), bạn có thể xem xét kỹ thuật preloading (tải trước) hoặc prefetching (tìm nạp trước) các chunk đó trong lúc trình duyệt nhàn rỗi để giảm độ trễ khi người dùng thực sự cần chúng. Các công cụ build như Webpack có các tùy chọn cấu hình cho việc này (ví dụ: sử dụng magic comments
/* webpackPrefetch: true */
hoặc/* webpackPreload: true */
trongimport()
).
Comments