jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

Webpack · Part 8 — Caching & Long-Term Caching

The file-naming strategy that lets browsers cache bundles for a year: [contenthash], deterministic moduleIds, extracting the runtime, and why splitting vendors keeps hashes stable. With an interactive contenthash cache simulator.

Once your app is live, returning users shouldn’t re-download code that didn’t change {Khi app đã live, user quay lại không nên tải lại code không đổi}. The trick is content-based file names: if the name changes only when the content changes, you can tell the browser to cache each file forever {Mẹo là tên file dựa trên nội dung: nếu tên chỉ đổi khi nội dung đổi, bạn có thể bảo trình duyệt cache mỗi file mãi mãi}. Get this wrong and users either see stale code or re-download everything on each deploy {Làm sai thì user hoặc thấy code cũ hoặc tải lại mọi thứ mỗi lần deploy}.

Edit code below and watch which file hashes change versus stay cached {Sửa code bên dưới và xem hash file nào đổi và cái nào vẫn cache}:


1. The caching strategy {Chiến lược caching}

The pattern is immutable files + hashed names {Mẫu là file bất biến + tên băm}. You configure your server to send Cache-Control: max-age=31536000, immutable for hashed assets {Bạn cấu hình server gửi Cache-Control: max-age=31536000, immutable cho asset băm}. Because the filename changes whenever the content does, a new deploy produces new names — the browser fetches the new files and keeps the unchanged ones from cache {Vì filename đổi mỗi khi nội dung đổi, một deploy mới tạo tên mới — trình duyệt tải file mới và giữ file không đổi từ cache}. HtmlWebpackPlugin (Part 4) rewrites the <script>/<link> tags to the new names automatically {HtmlWebpackPlugin (Phần 4) viết lại thẻ <script>/<link> sang tên mới tự động}.


2. [contenthash] {[contenthash]}

Use the [contenthash] token in output names — a hash derived from the file’s content {Dùng token [contenthash] trong tên output — một hash dẫn xuất từ nội dung file}:

output: {
  filename: "[name].[contenthash].js",
  chunkFilename: "[name].[contenthash].js", // for dynamic-import chunks
},
// and for extracted CSS (Part 4):
new MiniCssExtractPlugin({ filename: "[name].[contenthash].css" })

There’s also [hash] (whole-build hash — changes every build, bad) and [chunkhash] (per-chunk) {Còn có [hash] (hash cả-build — đổi mỗi build, tệ) và [chunkhash] (theo chunk)}. [contenthash] is the one you want — it changes the least {[contenthash] là cái bạn muốn — nó đổi ít nhất}. Toggle it off in the demo to see names freeze and the stale-cache risk {Tắt nó trong demo để thấy tên đông cứng và rủi ro cache cũ}.


3. Split vendors so they cache independently {Tách vendor để cache độc lập}

Your node_modules rarely change but make up most of your bytes {node_modules hiếm khi đổi nhưng chiếm phần lớn byte}. Keep them in a separate chunk (Part 6) so editing your app code doesn’t invalidate the giant vendor file {Giữ chúng trong chunk riêng (Phần 6) để sửa code app không làm vô hiệu file vendor khổng lồ}:

optimization: {
  splitChunks: { chunks: "all" },
}

In the demo, edit app code with “split vendors” on: only main re-downloads while vendors stays cached {Trong demo, sửa code app với “split vendors” bật: chỉ main tải lại trong khi vendors vẫn cache}.


4. Extract the runtime {Trích runtime}

Here’s the subtle bug {Đây là lỗi tinh vi}. Webpack’s runtime contains a manifest mapping module IDs to chunks {Runtime của Webpack chứa một manifest ánh xạ ID module tới chunk}. If that runtime lives inside your vendor chunk, then any app change updates the manifest and thus changes the vendor hash — defeating the whole point {Nếu runtime đó nằm trong chunk vendor, thì bất kỳ thay đổi app nào cũng cập nhật manifest và do đó đổi hash vendor — phá hỏng cả mục đích}. Extract it {Trích nó ra}:

optimization: {
  runtimeChunk: "single", // separate runtime.js
}

In the demo, turn runtimeChunk off and edit app code — notice vendors now changes too. Turn it on and vendors stays cached {Trong demo, tắt runtimeChunk và sửa code app — để ý vendors giờ cũng đổi. Bật lên và vendors vẫn cache}.


5. Deterministic module & chunk IDs {ID module & chunk tất định}

By default module IDs could shift when you add/remove a module, changing hashes of chunks that didn’t really change {Mặc định ID module có thể dịch khi bạn thêm/bớt module, đổi hash của chunk không thực sự đổi}. Production mode already uses deterministic IDs, but you can set it explicitly {Production mode đã dùng ID deterministic, nhưng bạn có thể đặt rõ}:

optimization: {
  moduleIds: "deterministic",
  chunkIds: "deterministic",
}

This gives short, stable IDs based on content, so unrelated edits don’t ripple into other chunks’ hashes {Điều này cho ID ngắn, ổn định dựa trên nội dung, nên sửa không liên quan không lan vào hash chunk khác}.


6. The complete caching config {Config caching hoàn chỉnh}

module.exports = {
  mode: "production",
  output: {
    filename: "[name].[contenthash].js",
    chunkFilename: "[name].[contenthash].js",
    clean: true,
  },
  optimization: {
    moduleIds: "deterministic",
    runtimeChunk: "single",
    splitChunks: { chunks: "all" },
  },
};

Pair this with Cache-Control: immutable on hashed files and a short cache on index.html (which references the hashed names) {Ghép với Cache-Control: immutable trên file băm và cache ngắn trên index.html (file tham chiếu tên băm)}. That’s the production caching recipe in five lines of optimization {Đó là công thức caching production trong năm dòng optimization}.


7. Exercises {Bài tập}

1. Every deploy, returning users re-download your 130 KB vendor bundle even though dependencies didn’t change. You split vendors but not the runtime. What’s the fix? {Mỗi deploy, user quay lại tải lại bundle vendor 130 KB dù phụ thuộc không đổi. Bạn tách vendor nhưng không tách runtime. Sửa thế nào?}

Solution {Lời giải}

Add optimization.runtimeChunk = "single" so the manifest lives in its own tiny file and app changes don’t alter the vendor hash {Thêm optimization.runtimeChunk = "single" để manifest ở file nhỏ riêng và thay đổi app không đổi hash vendor}.

2. You used [hash] instead of [contenthash]. Why do all files re-download on every deploy? {Bạn dùng [hash] thay vì [contenthash]. Vì sao mọi file tải lại mỗi deploy?}

Solution {Lời giải}

[hash] is a whole-build hash — it changes on every build, so every filename changes even if content didn’t. Use [contenthash] {[hash] là hash cả-build — đổi mỗi build, nên mọi filename đổi dù nội dung không. Dùng [contenthash]}.

3. You shipped with filename: "bundle.js" (no hash) and Cache-Control: immutable. Users report seeing old code after a deploy. Why? {Bạn ship với filename: "bundle.js" (không hash) và Cache-Control: immutable. User báo thấy code cũ sau deploy. Vì sao?}

Solution {Lời giải}

The name never changes, so the browser keeps serving the cached (stale) file. Add [contenthash] so new content gets a new name {Tên không đổi, nên trình duyệt cứ phục vụ file cache (cũ). Thêm [contenthash] để nội dung mới có tên mới}.

Stretch {Nâng cao}: in the simulator, disable runtimeChunk, edit app code, and confirm vendors re-downloads; then enable it and confirm only runtime and main change {trong trình mô phỏng, tắt runtimeChunk, sửa code app, và xác nhận vendors tải lại; rồi bật lên và xác nhận chỉ runtimemain đổi}.


Key takeaways {Điểm chính}

  • Use [contenthash] so filenames change only when content does {Dùng [contenthash] để filename chỉ đổi khi nội dung đổi}.
  • Split vendors so app edits don’t invalidate dependency bundles {Tách vendor để sửa app không làm vô hiệu bundle phụ thuộc}.
  • runtimeChunk: "single" keeps the manifest out of the vendor hash {runtimeChunk: "single" giữ manifest ngoài hash vendor}.
  • moduleIds: "deterministic" prevents unrelated edits from rippling {moduleIds: "deterministic" ngăn sửa không liên quan lan rộng}.
  • Serve hashed files with Cache-Control: immutable {Phục vụ file băm với Cache-Control: immutable}.

Next up {Tiếp theo}

Part 9 — Bundle analysis & performance: measuring what’s actually in your bundle with the analyzer, finding bloat, performance budgets, and prefetch/preload hints {Phần 9 — Phân tích bundle & hiệu năng: đo thực sự cái gì trong bundle bằng analyzer, tìm phình to, ngân sách hiệu năng, và gợi ý prefetch/preload}.