jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

Vite · Part 4 — Dependency Pre-bundling

Why Vite pre-bundles node_modules into .vite/deps: collapsing hundreds of module requests into one, converting CommonJS to ESM, and caching it all. Plus optimizeDeps include/exclude and when to clear the cache. With a pre-bundle visualizer.

Native ESM in dev has two problems with node_modules {ESM native khi dev có hai vấn đề với node_modules}. First, many packages ship hundreds of tiny files — importing one would trigger hundreds of requests {Một, nhiều package ship hàng trăm file nhỏ — import một cái sẽ kích hoạt hàng trăm request}. Second, lots of packages are still CommonJS, which the browser’s native ESM can’t run at all {Hai, nhiều package vẫn là CommonJS, thứ ESM native của trình duyệt không chạy được}. Pre-bundling solves both {Pre-bundle giải quyết cả hai}.

Run the pre-bundle step and watch hundreds of files collapse into a few cached ones {Chạy bước pre-bundle và xem hàng trăm file gộp lại thành vài file cache}:


1. The two problems {Hai vấn đề}

Problem 1: request explosion. lodash-es ships ~640 ESM modules {Vấn đề 1: bùng nổ request. lodash-es ship ~640 module ESM}. If Vite served them raw, import { debounce } from "lodash-es" would fan out into hundreds of nested requests — a brutal waterfall even on localhost {Nếu Vite phục vụ thô, import { debounce } from "lodash-es" sẽ tỏa thành hàng trăm request lồng — thác nước tàn bạo cả trên localhost}.

Problem 2: CommonJS. Many packages export with module.exports / require() {Vấn đề 2: CommonJS. Nhiều package xuất bằng module.exports / require()}. The browser’s import only understands ESM {import của trình duyệt chỉ hiểu ESM}. Something must convert CJS → ESM {Cần thứ gì đó chuyển CJS → ESM}.


2. The fix: pre-bundle once {Cách sửa: pre-bundle một lần}

On first dev start, Vite scans your source for imported dependencies and bundles each into a single optimized ESM file under node_modules/.vite/deps/ {Lần đầu khởi động dev, Vite quét source tìm dependency được import và bundle mỗi cái thành một file ESM tối ưu trong node_modules/.vite/deps/}:

node_modules/.vite/deps/
├─ lodash-es.js      ← ~640 files → 1
├─ react.js          ← CommonJS → ESM
├─ react-dom_client.js
└─ _metadata.json    ← hashes for cache validation

Now import { debounce } from "lodash-es" is one request to one file {Giờ import { debounce } from "lodash-es"một request tới một file}. This is why, in the Part 3 demo, react showed up as a single dep request {Đây là vì sao, trong demo Phần 3, react xuất hiện như một request dep đơn}.

Vite 8 uses Rolldown for pre-bundling (earlier versions used esbuild) {Vite 8 dùng Rolldown để pre-bundle (phiên bản cũ dùng esbuild)}. Either way it’s a fast, native-speed pass that runs in well under a second for most apps {Dù sao đó là một lượt nhanh tốc-độ-native chạy dưới một giây cho đa số app}.


3. It’s cached hard {Được cache cứng}

Pre-bundled deps get Cache-Control: max-age=31536000, immutable and a hash in _metadata.json {Dep đã pre-bundle nhận cache một năm immutable và một hash trong _metadata.json}. Vite skips re-bundling unless something relevant changes {Vite bỏ qua bundle lại trừ khi có gì liên quan thay đổi}. The cache invalidates when {Cache mất hiệu lực khi}:

  • package.json dependencies change {dependency trong package.json đổi}.
  • Lockfile changes (package-lock.json, pnpm-lock.yaml, etc.) {lockfile đổi}.
  • Relevant vite.config.ts fields change (e.g. optimizeDeps, resolve.alias) {field liên quan trong vite.config.ts đổi}.

4. optimizeDeps — when you must intervene {optimizeDeps — khi bạn phải can thiệp}

Auto-detection covers ~99% of cases {Tự phát hiện lo ~99% trường hợp}. The two escape hatches {Hai lối thoát}:

export default defineConfig({
  optimizeDeps: {
    // Vite missed it (e.g. imported only via a dynamic/string path)
    include: ["my-lib/submodule"],
    // already valid ESM, or breaks when pre-bundled
    exclude: ["my-esm-only-lib"],
  },
});
  • include — force a dependency to be pre-bundled when Vite’s scanner can’t find it (common with deep or conditional imports) {ép một dependency được pre-bundle khi scanner không tìm thấy (hay gặp với import sâu hoặc điều kiện)}.
  • exclude — keep a dependency out of pre-bundling (it’s already clean ESM, or pre-bundling breaks it) {giữ một dependency khỏi pre-bundle (đã ESM sạch, hoặc pre-bundle làm hỏng)}.

5. The “stale deps” fix everyone learns {Cách sửa “dep cũ” ai cũng học}

Occasionally deps act weird after an install or a branch switch — usually a stale pre-bundle cache {Thi thoảng dep hành xử kỳ lạ sau khi install hoặc đổi branch — thường là cache pre-bundle cũ}. The fix {Cách sửa}:

# clear the optimize cache and re-bundle
rm -rf node_modules/.vite
# or
vite --force

This is the Vite equivalent of “turn it off and on again,” and it’s safe {Đây là kiểu “tắt đi bật lại” của Vite, và an toàn}.


6. A secondary reload {Một lần reload phụ}

If, while browsing, you import a dependency Vite hadn’t pre-bundled yet, Vite bundles it on the fly and does a quick page reload to serve the new optimized file {Nếu khi duyệt, bạn import một dependency Vite chưa pre-bundle, Vite bundle nó tức thì và reload trang nhanh để phục vụ file tối ưu mới}. You’ll see a “new dependencies optimized” message in the terminal {Bạn sẽ thấy thông báo “new dependencies optimized” trong terminal}. To avoid it for known-but-lazy deps, list them in optimizeDeps.include {Để tránh với dep biết-trước-nhưng-lười, liệt kê trong optimizeDeps.include}.


7. Exercises {Bài tập}

1. Why would serving lodash-es as raw ESM in dev be slow, and how does pre-bundling fix it? {Vì sao phục vụ lodash-es dạng ESM thô khi dev sẽ chậm, và pre-bundle sửa thế nào?}

Solution {Lời giải}

It ships ~640 files → hundreds of nested requests (a deep waterfall). Pre-bundling collapses them into one optimized file = one request {Nó ship ~640 file → hàng trăm request lồng (thác nước sâu). Pre-bundle gộp thành một file tối ưu = một request}.

2. A dependency is CommonJS. Can the browser’s native import run it directly? What does Vite do? {Một dependency là CommonJS. import native của trình duyệt chạy trực tiếp được không? Vite làm gì?}

Solution {Lời giải}

No — native ESM can’t run CJS. During pre-bundling Vite converts CJS to ESM so import works {Không — ESM native không chạy CJS. Khi pre-bundle Vite chuyển CJS sang ESM để import chạy}.

3. After switching branches, a dependency throws odd errors. What’s the quickest safe fix? {Sau khi đổi branch, một dependency báo lỗi lạ. Cách sửa nhanh và an toàn nhất?}

Solution {Lời giải}

Clear the pre-bundle cache: rm -rf node_modules/.vite or vite --force {Xóa cache pre-bundle: rm -rf node_modules/.vite hoặc vite --force}.

Stretch {Nâng cao}: in the demo, run the pre-bundle and note the before/after request counts, then map each output file back to its raw source (ESM bundle vs CJS→ESM conversion) {trong demo, chạy pre-bundle và để ý số request trước/sau, rồi ánh xạ mỗi file output về source thô (bundle ESM vs chuyển CJS→ESM)}.


Key takeaways {Điểm chính}

  • Vite pre-bundles dependencies into node_modules/.vite/deps on first start {Vite pre-bundle dependency vào node_modules/.vite/deps lần đầu khởi động}.
  • It solves the request explosion (many files → one) and CommonJS → ESM {Nó giải quyết bùng nổ request (nhiều file → một) và CommonJS → ESM}.
  • The cache is hashed and invalidates on dependency/lockfile/config changes {Cache được hash và mất hiệu lực khi dependency/lockfile/config đổi}.
  • Use optimizeDeps.include/exclude only for the rare miss {Dùng optimizeDeps.include/exclude chỉ cho trường hợp hiếm bị sót}.
  • Stale deps? rm -rf node_modules/.vite or vite --force {Dep cũ? rm -rf node_modules/.vite hoặc vite --force}.

Next up {Tiếp theo}

Part 5 — HMR deep dive: how Hot Module Replacement swaps a single module without losing state, the import.meta.hot API, framework Fast Refresh, and HMR boundaries {Phần 5 — Đào sâu HMR: cách Hot Module Replacement thay một module mà không mất state, API import.meta.hot, Fast Refresh của framework, và ranh giới HMR}.