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 SplittingLazy 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ài addEventListener 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 Splittingcơ chế kỹ thuật để chia nhỏ code, còn Lazy Loadingnguyê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.lazySuspense)

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.lazySuspense.

  • 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ọi import() độ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ột fallback (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  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.lazySuspense.

// 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ởi React.lazy.
  • <Suspense fallback={...}> được đặt xung quanh nơi LazyHeavyComponent đượ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 trong fallback cho đến khi chúng sẵn sàng.
  • Ban đầu, showHeavyfalse, nên LazyHeavyComponent 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ọi import('./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.lazySuspense để 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ủa App.jsHomePage.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ằng React.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ủa AboutPage.js được tải về.
  • Sau khi tải xong, AboutPage hiển thị. Code của DashboardPage.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 trong Suspense) để 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.lazySuspense đượ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 */ trong import()).

Comments

There are no comments at the moment.