jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

Webpack · Part 6 — Code Splitting & Lazy Loading

Stop shipping one giant bundle: dynamic import() for route-level lazy loading, splitChunks for vendor and shared code, runtimeChunk, and magic comments for naming and prefetching. With an interactive code-splitting visualizer.

By default Webpack packs everything into one bundle {Mặc định Webpack đóng mọi thứ vào một bundle}. That’s fine for a tiny app, but it means users download the code for every page before they see any page {Ổn cho app nhỏ, nhưng nghĩa là user tải code của mọi trang trước khi thấy bất kỳ trang nào}. Code splitting breaks the bundle into chunks loaded on demand {Code splitting chia bundle thành các chunk tải theo yêu cầu}.

Compare a monolith with a split build, then navigate to lazy-load routes {So sánh monolith với bản tách, rồi điều hướng để lazy-load route}:


1. Three ways to split {Ba cách tách}

  1. Dynamic import() — you explicitly mark a split point; the imported module becomes its own chunk {import() động — bạn đánh dấu điểm tách rõ ràng; module được import thành chunk riêng}.
  2. splitChunks — Webpack automatically extracts shared/vendor code into separate chunks {splitChunks — Webpack tự trích code chung/vendor thành chunk riêng}.
  3. Multiple entries — manual, page-based splitting (Part 2) {Nhiều entry — tách thủ công theo trang (Phần 2)}.

The first two are what you use in practice {Hai cái đầu là cái bạn dùng thực tế}.


2. Dynamic import — lazy loading {Dynamic import — tải lười}

A static import is always in the initial bundle {Một import tĩnh luôn ở trong bundle ban đầu}. A dynamic import() returns a promise and tells Webpack to split that module into a separately-loadable chunk {Một import() động trả về promise và bảo Webpack tách module đó thành chunk tải riêng được}:

// static — bundled up front
import { Editor } from "./Editor";

// dynamic — its own chunk, fetched only when this runs
button.addEventListener("click", async () => {
  const { Editor } = await import("./Editor");
  new Editor().mount();
});

The classic use is route-based splitting: each route’s component loads only when the user navigates there {Cách dùng kinh điển là tách theo route: component của mỗi route chỉ tải khi user điều hướng tới}:

// React
const Editor = React.lazy(() => import("./routes/Editor"));

In the demo, switch to split mode and visit /editor — watch its 220 KB chunk download only on first visit {Trong demo, chuyển sang split mode và vào /editor — xem chunk 220 KB của nó tải chỉ ở lần đầu}.


3. splitChunks — automatic vendor/shared chunks {splitChunks — chunk vendor/chung tự động}

Your node_modules code (React, lodash…) changes rarely but is huge {Code node_modules (React, lodash…) hiếm khi đổi nhưng to}. Extract it into a vendors chunk so it caches independently of your app code {Trích nó thành chunk vendors để nó cache độc lập với code app}:

module.exports = {
  optimization: {
    splitChunks: {
      chunks: "all", // split both sync and async imports
    },
  },
};

chunks: "all" is the one setting that does 90% of the work {chunks: "all" là cài đặt duy nhất làm 90% công việc}. Webpack now pulls shared dependencies and node_modules into separate chunks, and if two routes both import the same util, it’s deduplicated into a shared chunk {Webpack giờ kéo phụ thuộc chung và node_modules vào chunk riêng, và nếu hai route cùng import một util, nó được khử trùng lặp vào chunk chung}.

You can tune groups {Bạn có thể tinh chỉnh nhóm}:

splitChunks: {
  chunks: "all",
  cacheGroups: {
    vendor: { test: /[\\/]node_modules[\\/]/, name: "vendors", priority: 10 },
  },
}

4. runtimeChunk — stable caching {runtimeChunk — cache ổn định}

Webpack injects a small runtime (the module loader) into a bundle {Webpack tiêm một runtime nhỏ (bộ tải module) vào bundle}. If it lives inside your vendor chunk, every app change can invalidate the vendor hash {Nếu nó nằm trong chunk vendor, mọi thay đổi app có thể làm vô hiệu hash vendor}. Extract it once {Trích nó ra một lần}:

optimization: {
  runtimeChunk: "single", // one shared runtime.js across all entries
}

Now the vendor chunk’s hash only changes when dependencies actually change — better long-term caching (Part 8) {Giờ hash của chunk vendor chỉ đổi khi phụ thuộc thực sự đổi — caching dài hạn tốt hơn (Phần 8)}.


5. Magic comments {Magic comment}

Webpack reads special comments inside import() to name and hint chunks {Webpack đọc các comment đặc biệt trong import() để đặt tên và gợi ý chunk}:

import(
  /* webpackChunkName: "editor" */
  /* webpackPrefetch: true */
  "./routes/Editor"
);
  • webpackChunkName gives the chunk a readable name instead of src_routes_Editor_js.123.js {cho chunk một tên đọc được thay vì src_routes_Editor_js.123.js}.
  • webpackPrefetch: true tells the browser to fetch the chunk during idle time, so it’s ready before the user clicks {bảo trình duyệt tải chunk lúc rảnh, để sẵn sàng trước khi user bấm}.
  • webpackPreload: true fetches it in parallel with the parent chunk (use sparingly) {tải song song với chunk cha (dùng tiết kiệm)}.

6. The trade-off {Đánh đổi}

Splitting isn’t free — each chunk is an extra HTTP request and a bit of overhead {Tách không miễn phí — mỗi chunk là một request HTTP thêm và chút overhead}. The goal is a small initial bundle (fast first paint) plus on-demand chunks for heavy, rarely-visited features {Mục tiêu là một bundle ban đầu nhỏ (first paint nhanh) cộng chunk theo yêu cầu cho tính năng nặng, ít ghé}. Don’t split every tiny module — split at route boundaries and around heavy dependencies (a chart library, a rich-text editor) {Đừng tách mọi module nhỏ — tách ở ranh giới route và quanh phụ thuộc nặng (thư viện biểu đồ, editor rich-text)}. The demo’s metrics show the initial-download win clearly {Số liệu của demo cho thấy lợi ích initial-download rõ ràng}.


7. Exercises {Bài tập}

1. Your app loads slowly because a 220 KB rich-text editor used on one page is in the main bundle. How do you defer it? {App tải chậm vì một editor rich-text 220 KB dùng ở một trang nằm trong bundle chính. Làm sao hoãn nó?}

Solution {Lời giải}

Load it with a dynamic import("./Editor") so it becomes its own chunk fetched only on that page {Tải bằng import("./Editor") động để nó thành chunk riêng chỉ tải ở trang đó}.

2. React and lodash are re-downloaded whenever you change app code. One config line to fix the caching? {React và lodash bị tải lại mỗi khi bạn đổi code app. Một dòng config để sửa caching?}

Solution {Lời giải}

optimization.splitChunks = { chunks: "all" } to extract node_modules into a separately-cached vendors chunk {optimization.splitChunks = { chunks: "all" } để trích node_modules vào chunk vendors cache riêng}.

3. You want a route’s chunk fetched during idle time so navigation feels instant. Which magic comment? {Bạn muốn chunk của một route tải lúc rảnh để điều hướng tức thì. Magic comment nào?}

Solution {Lời giải}

/* webpackPrefetch: true */ inside the import() {/* webpackPrefetch: true */ bên trong import()}.

Stretch {Nâng cao}: in the visualizer, compare the “initial download” metric between monolith and split modes, then visit every route in split mode and watch “downloaded so far” grow only as you go {trong trình trực quan, so số “initial download” giữa monolith và split, rồi vào mọi route ở split mode và xem “downloaded so far” chỉ tăng khi bạn đi tới}.


Key takeaways {Điểm chính}

  • Dynamic import() creates an on-demand chunk — the basis of lazy loading {import() động tạo chunk theo yêu cầu — nền tảng của tải lười}.
  • splitChunks: { chunks: "all" } auto-extracts vendor and shared code {splitChunks: { chunks: "all" } tự trích code vendor và chung}.
  • runtimeChunk: "single" stabilizes long-term caching {runtimeChunk: "single" ổn định caching dài hạn}.
  • Magic comments name chunks and trigger prefetch/preload {Magic comment đặt tên chunk và kích prefetch/preload}.
  • Split at route boundaries and around heavy deps, not everywhere {Tách ở ranh giới route và quanh phụ thuộc nặng, không phải khắp nơi}.

Next up {Tiếp theo}

Part 7 — Tree shaking & production mode: how Webpack drops unused exports, the role of ES modules and sideEffects, usedExports, minification with Terser, and why mode: "production" matters {Phần 7 — Tree shaking & production mode: cách Webpack bỏ export không dùng, vai trò của ES module và sideEffects, usedExports, minify với Terser, và vì sao mode: "production" quan trọng}.