Caching Frontend Đúng Cách: Bí Quyết Tối Ưu Hiệu Suất Cho Các Dự Án Lớn

05 tháng 4, 2026·10 phút đọc

Bài viết này sẽ giải thích chi tiết cách bộ nhớ đệm trình duyệt và tiêu đề HTTP hoạt động, cũng như cách sử dụng chiến lược Stale-While-Revalidate và Service Workers để kiểm soát chương trình. Đặc biệt, nó cung cấp các hướng dẫn thực tế về việc không nên cache, xử lý cache invalidation và bảo vệ hiệu suất khi nền tảng của bạn phục vụ hàng triệu người dùng.

Caching Frontend Đúng Cách: Bí Quyết Tối Ưu Hiệu Suất Cho Các Dự Án Lớn

Caching là một chủ đề mà hầu hết các lập trình viên Frontend đều nghĩ mình đã hiểu rõ, cho đến khi đối mặt với các vấn đề sản xuất như dữ liệu lỗi thời hoặc server bị quá tải. Khi làm việc với các nền tảng phục vụ hàng triệu người dùng, caching không còn là một tính năng "tốt để có" mà là yếu tố sống còn để đảm bảo hiệu suất và trải nghiệm người dùng mượt mà. Bài viết này tổng hợp những gì tôi đã học được về caching Frontend, được viết theo cách tôi mong muốn ai đó đã giải thích cho tôi khi mới bắt đầu sự nghiệp.

Hiểu Đúng Về Những Gì Bạn Đang Cache

Trước khi đụng vào bất kỳ tiêu đề HTTP hay viết một dòng code Service Worker nào, hãy tự hỏi một câu hỏi quan trọng: "Chi phí khi cung cấp dữ liệu lỗi thời là bao nhiêu?". Câu trả lời này quyết định mọi thứ. Bởi vì caching luôn là sự đánh đổi giữa tính mới (freshness) và hiệu năng. Sai lầm phổ biến nhất của các lập trình viên là đối xử với tất cả tài nguyên như nhau. Hãy hình dung chúng như sau:

  • Tài nguyên tĩnh (Static assets): (JS bundles, CSS, font, ảnh) - có thể cache mạnh mẽ, thậm chí mãi mãi nếu bạn có phiên bản hóa đúng cách.
  • Phản hồi API: Phụ thuộc hoàn toàn vào tần suất thay đổi dữ liệu và mức độ quan trọng nếu người dùng nhìn thấy dữ liệu cũ một chút.
  • Tài liệu HTML: Thường không nên cache mạnh, đặc biệt cho các ứng dụng cần xác thực.
  • Dữ liệu dành riêng cho người dùng: Hiếm khi nên cache nếu không suy nghĩ kỹ.

Hãy đặt đúng mô hình tư duy này trước tiên, mọi thứ khác sẽ theo nó.

Bộ Nhớ Đệm Trình Duyệt (Browser Cache) Và Tiêu Đề HTTP

Bộ nhớ đệm trình duyệt là lớp cache mạnh mẽ nhất, nằm giữa người dùng và server, được kiểm soát hoàn toàn bằng các tiêu đề phản hồi HTTP.

Cache-Control

Đây là tiêu đề bạn sẽ sử dụng nhiều nhất. Dưới đây là ý nghĩa của các chỉ thị quan trọng:

Cache-Control: max-age=31536000, immutable

  • max-age: Cho trình duyệt biết bao nhiêu giây để giữ lại tài nguyên này trước khi coi nó là lỗi thời.
  • immutable: Nói cho trình duyệt đừng tốn công kiểm tra lại ngay cả khi thực hiện làm mới cứng (hard refresh), vì nội dung sẽ không bao giờ thay đổi.

Sử dụng kết hợp này cho các tài nguyên tĩnh có phiên bản hóa như JS bundles, CSS, hoặc file ảnh có hash tên (ví dụ: main.a3f9c2.js). Hash thay đổi mỗi lần build, nên URL thay đổi, bạn không bao giờ phục vụ code lỗi thời. Cache mãi mãi.

Cache-Control: no-cache

Mặc dù tên gọi nói "không cache", nhưng thực tế nó có nghĩa là "cache nó, nhưng phải kiểm tra với server mỗi lần trước khi dùng". Server có thể phản hồi với 304 Not Modified và trình duyệt sẽ dùng phiên bản đã cache. Không cần tải lại toàn bộ.

Sử dụng cho tài liệu HTML. Bạn muốn trình duyệt luôn kiểm tra xem có phiên bản mới không, nhưng vẫn hưởng lợi từ cache khi không có gì thay đổi.

Cache-Control: no-store

Điều này thực sự có nghĩa là "không cache". Không có gì được lưu trữ ở đâu. Sử dụng cho dữ liệu nhạy cảm, phản hồi xác thực, bất cứ thứ gì bạn không muốn nằm trong bộ nhớ cache.

ETag và Last-Modified

Chúng hoạt động cùng với Cache-Control để kiểm tra tính hợp lệ (revalidation). Khi trình duyệt hỏi "đã có gì thay đổi chưa?", server sử dụng chúng để trả lời.

ETag: "abc123" Last-Modified: Mon, 01 Jan 2024 00:00:00 GMT

Trình duyệt sẽ gửi lại If-None-Match: "abc123" hoặc If-Modified-Since trong yêu cầu tiếp theo. Nếu không có gì thay đổi, server trả về 304 và tiết kiệm băng thông gửi lại toàn bộ phản hồi.

Tại quy mô lớn, những khoản tiết kiệm nhỏ cộng lại sẽ tạo ra sự giảm tải đáng kể lên server.

Stale-While-Revalidate: Chiến Lược Cực Kỳ Mạnh Mẽ

Đây là một trong những chiến lược caching ít được sử dụng nhất nhưng lại cực kỳ mạnh mẽ trong các codebase Frontend.

Cache-Control: max-age=60, stale-while-revalidate=300

Cách hoạt động của nó như sau: Trong 60 giây đầu tiên, phục vụ từ cache, không cần hỏi. Từ 60 đến 300 giây, phục vụ phiên bản lỗi thời ngay lập tức nhưng đồng thời kích hoạt một yêu cầu ở nền để kiểm tra lại. Sau 300 giây, nó trở nên lỗi thời và phải được kiểm tra lại trước khi phục vụ.

Người dùng nhận được phản hồi tức thì. Bộ nhớ đệm được cập nhật ở nền. Không có spinner tải, không có đợi.

Mẫu này hoàn hảo cho dữ liệu thay đổi đôi khi nhưng không cần thời gian thực. Hãy nghĩ đến menu điều hướng, dữ liệu cấu hình, nội dung cập nhật vài lần một ngày. Người dùng luôn có trải nghiệm nhanh và dữ liệu vẫn ở mức tương đối mới mẻ.

Bạn cũng có thể nhận thấy mẫu này từ cấu hình staleTimegcTime của TanStack Query. Ý tưởng cốt lõi là y hệt, chỉ được áp dụng ở lớp JavaScript thay vì lớp HTTP.

Service Workers: Kiểm Soát Lập Trình

Tiêu đề HTTP cho phép bạn kiểm soát theo cách khai báo (declarative). Service Workers cho phép bạn kiểm soát theo cách lập trình (programmatic). Đây là sự khác biệt đáng kể.

Một Service Worker nằm giữa ứng dụng của bạn và mạng, chặn mọi yêu cầu. Bạn quyết định chuyện gì xảy ra với từng yêu cầu.

self.addEventListener('fetch', (event) => { event.respondWith( caches.match(event.request).then((cachedResponse) => { if (cachedResponse) { return cachedResponse } return fetch(event.request) }) ) })

Đây là chiến lược "Cache First" đơn giản. Kiểm tra cache trước, nếu không tìm thấy thì mới tìm trên mạng. Cho một nền tảng phục vụ hàng triệu người dùng, điều này có nghĩa là những người truy cập lại thường xuyên thậm chí không cần chạm vào server cho các tài nguyên tĩnh.

Các Chiến Lược Caching Với Service Workers

  • Cache First: Phục vụ từ cache, nhảy sang mạng nếu không tìm thấy. Tốt cho tài nguyên tĩnh hiếm khi thay đổi.
  • Network First: Thử mạng trước, nhảy sang cache nếu ngoại tuyến. Tốt cho dữ liệu API mà tính mới mẻ quan trọng nhưng cần hỗ trợ ngoại tuyến.
  • Stale While Revalidate: Phục vụ từ cache ngay lập tức, cập nhật cache ở nền. Tốt cho nội dung không quan trọng về tính mới mẻ nhưng tốc độ là ưu tiên.
  • Cache Only: Chỉ phục vụ từ cache. Hữu ích cho tài nguyên bạn đã pre-cache trong quá trình cài đặt.
  • Network Only: Luôn đi đến mạng. Cho các yêu cầu không nên cache, như analytics hoặc các endpoint thanh toán.

Chọn chiến lược cho từng loại tài nguyên, không phải toàn cục. Một chiến lược duy nhất cho tất cả là gần như luôn sai lầm.

Pre-caching vs Runtime Caching

Pre-caching xảy ra khi Service Worker cài đặt. Bạn liệt kê rõ ràng các tài nguyên cần cache trước.

self.addEventListener('install', (event) => { event.waitUntil( caches.open('v1').then((cache) => { return cache.addAll([ '/', '/styles/main.css', '/scripts/main.js', ) }) ) })

Runtime caching diễn ra động theo từng yêu cầu khi chúng đến. Bạn cache phản hồi khi chúng được tải, nên các tài nguyên thường xuyên truy cập sẽ dần được đưa vào cache theo thời gian.

Đối với hầu hết các nền tảng, bạn cần cả hai. Pre-cache "hạt nhân" quan trọng, runtime cache tất cả những thứ còn lại.

Những Gì Tuyệt Đối Không Nên Cache

Đây là phần khiến mọi người mắc sai lầm. Caching sai chỗ gây ra các lỗi rất khó debug trong sản xuất.

Tuyệt đối không cache mạnh mẽ:

  • Mã xác thực (Authentication tokens) hoặc dữ liệu phiên (session data)
  • Các endpoint thanh toán và giao dịch
  • Dữ liệu cá nhân hóa dành riêng cho người dùng
  • Bất cứ thứ gì thay đổi theo người dùng hoặc theo phiên
  • Cấu hình A/B test nếu chúng cần thời gian thực

Tôi từng thấy các đội nhóm cache phản hồi API bao gồm cả các đặc quyền của người dùng. Kết quả là người dùng thấy nội dung họ không nên có quyền truy cập, hoặc không thấy nội dung họ vừa mua. Tại quy mô lớn, đó không chỉ là một lỗi, mà là một vấn đề về niềm tin.

Khi nghi ngờ, đừng cache nó, hoặc sử dụng no-cache để đảm bảo việc kiểm tra lại vẫn diễn ra.

Cache Invalidation - Phần Khó Nhất

Có một câu nói nổi tiếng trong khoa học máy tính: "Có chỉ hai thứ khó trong lập trình: đó là đặt tên cho các thứ và hủy cache (cache invalidation)".

Nó vui vẻ nhưng đúng đắn.

Đối với tài nguyên tĩnh, các tên file có hash nội dung giải quyết hoàn toàn vấn đề này. Deploy mới, hash mới, URL mới, mục cache mới. Cái cũ tự động hết hạn.

Đối với phản hồi API và bộ nhớ đệm Service Worker, bạn cần một chiến lược phiên bản hóa.

const CACHE_VERSION = 'v2'

self.addEventListener('activate', (event) => { event.waitUntil( caches.keys().then((cacheNames) => { return Promise.all( cacheNames .filter((name) => name !== CACHE_VERSION) .map((name) => caches.delete(name)) ) }) ) })

Mỗi khi bạn deploy, hãy tăng phiên bản cache. Sự kiện activate sẽ dọn dẹp các cache cũ. Người dùng sẽ nhận được dữ liệu mới khi ghé thăm tiếp theo mà không cần bạn xóa thủ công.

Caching Tại Quy Mô Lớn - Thay Đổi Thực Sự

Khi bạn đang xây dựng cho 10 triệu người dùng, các nguyên lý cơ bản không thay đổi. Nhưng hậu quả của việc làm sai là được khuếch đại đáng kể.

Một vài điều tôi đã học được từ việc vận hành ở quy mô đó:

  • Đo lường trước khi tối ưu hóa. Sử dụng Chrome DevTools, Lighthouse và dữ liệu RUM (Real User Monitoring) của bạn để hiểu tỷ lệ hit cache thực tế là bao nhiêu. Đừng đoán mò.
  • CDN caching và browser caching là hai lớp khác nhau. CDN có các tiêu đề cache riêng, thường tách biệt với những gì trình duyệt thấy. Hiểu cả hai. Thiết lập sai CDN có thể khiến hàng triệu người dùng bỏ qua bộ nhớ đệm trình duyệt hoàn toàn.
  • Hiện tượng dồn đập cache (cache stampedes) là có thật. Khi một tài nguyên cache quan trọng hết hạn cùng lúc cho hàng triệu người dùng, họ sẽ tất cả cùng tấn công vào server. Stale-while-revalidate và thời gian hết hạn bị xáo trộn (jittered) giúp ngăn chặn điều này.
  • Theo dõi tỷ lệ hit cache. Nếu thấp, bạn đang bỏ qua hiệu suất. Nếu quá cao và bạn thấy phàn nàn về dữ liệu lỗi thời, thì TTL của bạn quá hà khắc.

Lời Kết

Caching không phải là một tính năng "đặt rồi quên". Nó là một quyết định kỹ thuật liên tục chạm đến hiệu suất, tính chính xác và trải nghiệm người dùng cùng một lúc.

Bắt đầu với các tiêu đề HTTP và làm cho chúng đúng. Lớp hóa chiến lược Stale-While-Revalidate cho các tài nguyên phù hợp. Thêm Service Workers khi bạn cần hỗ trợ ngoại tuyến hoặc kiểm soát chi tiết hơn. Và luôn nghĩ về việc hủy cache (invalidation) trước khi nghĩ về việc cache.

Những lập trình viên làm đúng việc này không phải là những người biết nhiều nhất về các chỉ thị cache. Họ là những người đặt câu hỏi đúng trước tiên.

Chi phí khi cung cấp dữ liệu lỗi thời là bao nhiêu ở đây?

Hãy trả lời chân thành cho từng tài nguyên, và phần còn lại sẽ theo đó.

Bài viết được tổng hợp và biên soạn bằng AI từ các nguồn tin tức công nghệ. Nội dung mang tính tham khảo. Xem bài gốc ↗