Tìm hiểu sâu về HTTP Caching

Tìm nạp tài nguyên qua mạng internet vừa chậm, vừa tốn kém. Các phản hồi nặng yêu cầu nhiều vòng lặp khứ hồi (roundtrips) giữa máy khách và máy chủ (server). Điều này làm khả năng hiện diện của chúng bị trì hoãn, và đồng thời làm trễ thời điểm trình duyệt có thể xử lý chúng. Nó cũng làm phát sinh chi phí dữ liệu (data cost) của người duyệt web. Hệ quả là, khả năng cache và tái sử dụng các tài nguyên đã được tìm nạp trước đó là một yếu tố quan trọng trong việc tối ưu hóa hiệu suất, tốc độ.

Tin tốt là tất cả các trình duyệt đều đã triển khai HTTP cache. Tất cả những gì bạn cần là đảm bảo mỗi phản hồi của máy chủ cung cấp các chỉ thị header HTTP chính xác để hướng dẫn trình duyệt, nhờ vậy nó biết được có thể cache một phản hồi vào lúc nào trong bao lâu.

Lưu ý: Nếu bạn sử dụng WebView để tìm nạp và hiển thị nội dung web trong ứng dụng của bạn, bạn có thể cần cung cấp các cờ tùy chỉnh bổ sung để đảm bảo rằng HTTP cache được bật, kích cỡ của nó được thiết lập với giá trị hợp lý để khớp với nhu cầu của riêng bạn, và bộ nhớ cache được duy trì. Tham khảo thêm các tài liệu căn bản để chắc chắn các cài đặt của bạn là chính xác.

yêu cầu http

Khi máy chủ trả về một phản hồi, nó cũng có thể phát ra một tập hợp các header HTTP, dùng để mô tả kiểu nội dung, độ dài, các chỉ thị caching, token xác minh (ETag), và nhiều thứ khác nữa. Lấy ví dụ, trong trao đổi ở trên, máy chủ trả về phản hồi có kích cỡ 1024-byte (content-length), hướng dẫn máy khách cache nó trong khoảng thời gian 120 giây (cache-control: max-age), và cung cấp token xác thực “x234dff” (ETag) mà có thể được sử dụng sau khi phản hồi hết hạn (expired) để kiểm tra xem tài nguyên đã có bất kỳ sửa đổi nào hay chưa (modified).

Xác thực các phản hồi đã được cache bằng ETags

Tóm tắt (TL;DR)

  • Máy chủ sử dụng header HTTP ETag để trao đổi token xác thực (validation).
  • Token xác thực cho phép kiểm tra xem liệu tài nguyên đã được cập nhật hay chưa một cách hiệu quả: sẽ không cần phải tải dữ liệu (đã được cache) qua mạng nữa nếu tài nguyên chưa có bất kỳ thay đổi nào.

Giả sử đã qua 120 giây kể từ lần tìm nạp đầu tiên và trình duyệt bắt đầu thực hiện một tìm nạp mới cho cùng nguồn tài nguyên (same resource). Đầu tiên, trình duyệt kiểm tra local cache và tìm kiếm phản hồi đã có trước đó. Không may, trình duyệt không thể sử dụng phản hồi trước đó bởi vì phản hồi này giờ đã hết hạn. Tại thời điểm đó, trình duyệt có thể nhanh chóng gửi đi một yêu cầu mới và tìm nạp phản hồi mới đầy đủ (full response). Tuy nhiên, điều này không hiệu quả, bởi vì nếu dữ liệu của tài nguyên không có gì thay đổi, bạn chẳng có lý do gì để tải cùng một thông tin mà đã được cache sẵn ở đấy rồi!

Đây là vấn đề mà các token xác thực, như được chỉ định trong header ETag được thiết kế để khắc phục. Máy chủ tạo ra và trả về mã token tùy ý, nó thường là một hash hoặc dấu nhận biết duy nhất (fingerprint) cho các nội dung của file. Máy khách không cần biết cách dấu nhận biết duy nhất này được tạo ra như thế nào; nó chỉ cần hoàn thành nhiệm vụ là gửi nó trở lại máy chủ trong lần yêu cầu kế tiếp. Nếu fingerprint vẫn y nguyên, điều đó có nghĩa là tài nguyên chưa có gì thay đổi, và bạn có thể không cần phải tải tài nguyên đó qua mạng internet nữa.

HTTP cache control

Trong ví dụ trước, máy khách tự động cung cấp token ETag trong header yêu cầu HTTP có tên “If-None-Match/Nếu-Không-Khớp”. Máy chủ kiểm tra token một lần nữa cho tài nguyên hiện tại (current resource). Nếu token không thay đổi, máy chủ trả về phản hồi “304 Not Modified / Không Thay Đổi”, cái này sẽ nói cho trình duyệt biết rằng phản hồi mà nó có trong cache không thay đổi và có thể được làm mới lại bằng cách bổ sung thêm thời gian cho cache là 120 giây. Lưu ý là khi ấy bạn không tải lại phản hồi, điều đó giúp bạn tiết kiệm thời gian và băng thông.

Ví dụ trong thực tế. Đường link này: https://code.kiencang.net/cache-demo/60s/cache-control-60.html có thời gian cache (max-age) cho 4 tài nguyên là một ảnh (.jpg), một style (.css), hai font (.ttf) khoảng 60 giây. Sau thời gian này cache đã hết hạn, bạn thử tải lại trang lần nữa, kết quả là: dù trình duyệt biết tài nguyên cần tải đã có cache nhưng vì hết hạn nên nó không thể lấy tài nguyên cache ngay lập tức, nó sẽ hỏi lại máy chủ web xem tài nguyên đó có thay đổi gì không thông qua mã xác thực ETag. Kết quả ETag vẫn giữ nguyên (máy chủ trả về kết quả 304), khi đó trình duyệt biết rằng nó được phép dùng lại cache nó đang có và tiếp tục gia hạn thêm thời gian sống của cache này:

Tài nguyên trong cache trả về kết quả 304
tap-viet.ttf và thu-phap.ttf là những file nặng, nhờ việc xác thực ETag (chỉ tốn 275 Byte dữ liệu trao đổi) mà trình duyệt biết rằng nó không cần tải lại 2 file này. Điều tương tự cũng xảy ra với file .css và .jpg

Là người lập trình web, làm thế nào bạn tận dụng được việc xác nhận lại một cách có hiệu quả? Trình duyệt đã làm mọi việc thay mặt chúng ta. Trình duyệt tự động phát hiện nếu token xác thực đã được chỉ định trước đó, nó nối thêm token xác thực vào yêu cầu gửi đi, và nó cập nhật dấu thời gian cache khi cần, dựa trên phản hồi nhận được từ máy chủ. Chỉ còn một điều duy nhất cần phải làm là đảm bảo rằng máy chủ cung cấp các token ETag cần thiết. Kiểm tra tài liệu hướng dẫn cho máy chủ của bạn để có được các cờ tùy chỉnh cần thiết.

Mẹo cần lưu ý: Dự án HTML5 Boilerplate bao gồm các file tùy chỉnh mẫu cho tất cả các máy chủ phổ biến nhất với chú thích chi tiết cho từng cờ tùy chỉnh và cài đặt. Tìm máy chủ ưa thích của bạn trong danh sách, tìm kiếm các thiết lập phù hợp, và copy/xác thực máy chủ của bạn đã được tùy chỉnh theo các cài đặt được khuyến nghị.

Cache-Control

Tóm tắt:

  • Từng tài nguyên có thể định nghĩa chính sách cache của nó thông qua header HTTP có tên Cache-Control.
  • Cache-Control đưa ra chỉ thị điều khiển cho biết ai có thể cache phản hồi, dưới điều kiện cụ thể nào, và trong thời gian bao lâu.

Dưới cái nhìn về tối ưu hóa hiệu suất, yêu cầu tốt nhất là yêu cầu mà bạn không cần phải giao tiếp với máy chủ: một bản sao chép cục bộ của phản hồi (dữ liệu cache trên thiết bị của người dùng) cho phép bạn loại bỏ tất cả các vấn đề về độ trễ mạng (network latency) và tránh lãng phí dữ liệu (data charges) do phải trao đổi dữ liệu. Để đạt được điều đó, chỉ thị HTTP cho phép máy chủ trả về các chỉ thị Cache-Control, cái sẽ điều khiển cách (và trong bao lâu) thì trình duyệt và các nền tảng cache trung gian khác có thể cache một phản hồi cụ thể.

Lưu ý: Header Cache-Control được định nghĩa trong các chỉ thị HTTP/1.1 và được dùng để thay thế các header trước đó (chẳng hạn như Expires) dùng để định nghĩa các chính sách cache phản hồi. Tất cả các trình duyệt hiện đại đều đã hỗ trợ Cache-Control, đó là tất cả những gì mà chúng ta cần.

HTTP Cache-Control

“no-cache” và “no-store”

“no-cache” chỉ thị rằng một phản hồi trả về không thể được sử dụng để đáp ứng yêu cầu tiếp theo (subsequent request) cho cùng một URL mà chưa kiểm tra trước với máy chủ để xem phản hồi đó đã thay đổi nội dung hay chưa (tài nguyên có bất cứ sửa đổi nào hay không). Hệ quả là, nếu có token xác thực phù hợp (ETag), no-cache phát sinh một vòng lặp khứ hồi để xác thực phản hồi đã được cache, nhưng nó vẫn có thể loại bỏ hành vi tải về nếu tài nguyên vẫn chưa thay đổi.

Ví dụ trong thực tế. Đường link này: https://code.kiencang.net/cache-demo/10phut/cache-control-10phut.html có thời gian cache (max-age) cho 2 style (thaydoi.css và giunguyen.css) là 10 phút. Trong thời gian dưới 10 phút sau lượt tải lần đầu tôi sẽ thử tải lại (lần tải thứ hai) để xem thông tin về phản hồi. Rồi sau đó sửa lại một CSS, và một cái giữ nguyên (Cache-Control cho cả hai CSS đó là “no-cache”) rồi tải lại lần nữa (lần tải thứ ba).

ETag của thaydoi.css khi chưa chỉnh sửa (mã phản hồi / status code là 304 trong lần tải thứ hai / máy chủ web kiểm tra Etag và thấy nó giữ nguyên nên trả về phản hồi Not Moified):

ETag giữ nguyên

Sau khi sửa thaydoi.css, dù vẫn còn trong thời gian cache, nhưng trình duyệt kiểm tra ETag đã thay đổi nên nó sẽ tải lại tài nguyên này (mã phản hồi / status code là 200):

Etag đã thay đổi

Hai CSS cũng có mã phản hồi khác nhau (cả hai đều phải kiểm tra ETag, tài nguyên không thay đổi có mã phản hồi 304, tài nguyên thay đổi có mã phản hồi là 200):

Lưu ý để tránh nhầm lẫn: các tài nguyên không cần phải kiểm tra ETag (không có thuộc tính no-cache và thời gian cache chưa hết hạn) thì mã phản hồi trả về cũng là 200 nhưng bạn sẽ thấy ở phần size ghi rõ là memory cache, còn những cái cần kiểm tra ETag vì vẫn có trao đổi dữ liệu để kiểm tra nên dù mã phản hồi là gì vẫn có trao đổi dữ liệu dù nhỏ (nếu rơi vào trường hợp 304):

các mã phản hồi khác nhau

Vì 2 file CSS đưa vào thử nghiệm đều có kích cỡ nhỏ, nên để chắc chắn hơn tôi làm tăng kích cỡ của nó để xem khi chỉnh sửa ETag, lượng dữ liệu có trao đổi nhiều không qua đó sẽ khẳng định được 100% về chuyện tải về.

Đây là lượng dữ liệu của cả hai trong lần tải đầu tiên (chưa có cache), bạn quan tâm đến KB và thời gian tải về:

lượt tải dữ liệu lần đầu chưa có cache

Khi được cache (chỉ có một lượng dữ liệu rất nhỏ trao đổi để xác thực ETag):

Khi chỉnh sửa một file và giữ nguyên một file (dù trong thời gian cache, file thaydoi.css đã có ETag khác nên vẫn phải tải về đầy đủ dữ liệu là 69,9 KB còn file giunguyen.css thì vẫn giữ nguyên ETag nên dữ liệu vẫn lấy từ cache):

Ngược lại, “no-store” đơn giản hơn nhiều. Nó đơn giản là không cho phép trình duyệt và tất cả các nền tảng cache trung gian khác lưu trữ bất cứ phiên bản nào của phản hồi trả về, ví dụ dữ liệu bao gồm thông tin riêng tư hoặc dữ liệu ngân hàng. Bất cứ khi nào người dùng yêu cầu tài nguyên này, một yêu cầu sẽ được gửi đến máy chủ và một phản hồi đầy đủ (tươi mới) sẽ được tải về.

“public” và “private”

Nếu phản hồi được đánh dấu là “public”, thì nó có thể được cache, thậm chí ngay cả khi nó có HTTP authentication liên kết với nó, và thậm chí khi mã trạng thái phản hồi (response status code) không có khả năng cache theo cách thông thường. Trong hầu hết trường hợp, “public” là không cần thiết, vì các thông tin cache rõ ràng (chẳng hạn như “max-age”) đã chỉ ra rằng dù sao phản hồi là có khả năng cache.

Ngược lại, trình duyệt có thể cache phản hồi “private”. Dù vậy, các phản hồi này thường dành riêng cho một người dùng cụ thể, vì thế một cache trung gian không được phép cache chúng. Lấy ví dụ, trình duyệt của người dùng có thể cache một trang HTML với thông tin riêng tư của người dùng, nhưng CDN không thể cache trang.

“max-age”

Đây là hướng dẫn cụ thể về thời gian tối đa tính theo giây mà “phản hồi đã tìm nạp” được phép sử dụng lại tính từ thời điểm có yêu cầu. Lấy ví dụ, “max-age=60” chỉ thị rằng phản hồi có thể được cache và sử dụng lại trong vòng 60 giây kế tiếp.

Xác định chính sách Cache-Control tối ưu

chính sách cache-control tối ưu

Chú thích:

  • Hàng 1: Reusable response? Tái sử dụng phản hồi?
  • Hàng 2: Nếu Không (No) thì áp dụng “no-store”, nếu (Yes) thì hỏi tiếp: Revalidate each time? / Có xác thực lại mỗi lần tìm nạp hay không?
  • Hàng 3: Nếu Có thì áp dụng “no-cache”, còn nếu Không thì hỏi tiếp: Cacheable by intermediate caches? / Có khả năng cache bởi các nền tảng cache trung gian?
  • Hàng 4: Nếu Có thì áp dụng “public”, nếu Không thì áp dụng “private”
  • Hàng 5: Tối đa hóa thời gian sống của cache? (maximum cache lifetime?)
  • Hàng 6: Đưa ra chỉ thị max-age=…
  • Hàng 7: Thêm header ETag

Chúng ta sẽ sử dụng cây ra quyết định ở trên để xác định chính sách cache tối ưu cho một tài nguyên cụ thể, hoặc một thiết lập cho các tài nguyên mà ứng dụng của bạn sử dụng. Lý tưởng nhất, bạn phải hướng đến việc cache càng nhiều phản hồi càng tốt trên máy khách với thời gian cache tối đa trong khả năng, và cung cấp mã token xác thực cho từng phản hồi để bật tính năng xác thực lại (revalidation) một cách hiệu quả.

Các chỉ thị Cache-Control & giải thích ý nghĩa
max-age=86400Phản hồi có thể được cache bởi trình duyệt và bất kỳ nền tảng cache trung gian nào / intermediary cache (cái này là “public”) trong thời gian 1 ngày (60 giây x 60 x 24).
private, max-age=600Phản hồi có thể được cache bởi trình duyệt máy khách trong vòng chỉ 10 phút (60 giây x 10).
no-storePhản hồi (dữ liệu trả về từ máy chủ) không được phép cache và phải tìm nạp đầy đủ mỗi khi có yêu cầu.

Theo HTTP Archive, trong số 300 ngàn website được truy cập nhiều nhất (theo thứ hạng Alexa), trình duyệt có thể cache gần một nửa tất cả các phản hồi đã tải xuống, điều đó giúp tiết kiệm rất nhiều cho các lần truy cập lặp lại. Tất nhiên, điều đó không có nghĩa là ứng dụng cụ thể của bạn chỉ có thể cache 50% mà thôi. Một số website có thể cache nhiều hơn 90% các tài nguyên của họ, trong khi các trang khác có thể có nhiều dữ liệu riêng tư (private) hoặc nhạy cảm với thời gian (time-sensitive data), đây đều là dữ liệu không nên cache một chút nào.

Bạn cần kiếm tra trang của bạn để xác định các tài nguyên nào có thể cache và đảm bảo được rằng chúng trả về Cache-Control và ETag header chính xác.

Vô hiệu hóa và cập nhật các phản hồi đã được cache

Tóm tắt:

  • Các phản hồi đã được cache cục bộ trên thiết bị của người dùng (local cached) vẫn được sử dụng cho đến khi tài nguyên bị xem là “hết hạn”.
  • Nhúng một file fingerprint vào trong URL cho phép bạn bắt buộc máy khách phải cập nhật phiên bản mới của phản hồi.
  • Từng ứng dụng cần phải định nghĩa hệ thống cấp bậc cache của nó để tối ưu hóa hiệu suất, tốc độ.

Tất cả các yêu cầu HTTP mà trình duyệt thực hiện trước tiên được gửi đến cho bộ nhớ đệm trình duyệt để kiểm tra xem liệu rằng có một phản hồi nào đã được cache (mà vẫn hợp lệ / valid) để có thể sử dụng cho việc hoàn thành yêu cầu hay không. Phản hồi đọc từ cache loại bỏ cả độ trễ mạng và chi phí dữ liệu mà việc tải qua mạng sẽ phát sinh.

Tuy nhiên, nếu bạn muốn cập nhật hoặc làm mất hiệu lực của phản hồi đã được cache thì phải làm thế nào? Lấy ví dụ, giả sử bạn nói với trình duyệt của người dùng cache file stylesheet CSS trong vòng 24 giời (max-age=86400), nhưng bên thiết kế của bạn vừa mới đưa ra một bản cập nhật mà bạn muốn nó khả dụng cho tất cả người dùng. Làm thế nào bạn thông báo cho tất cả người dùng, những người hiện đang có bản sao chép cache “cũ” của CSS để cập nhật cache của họ? Bạn không thể, ít nhất là khi không thay đổi URL của tài nguyên.

Sau khi trình duyệt cache phản hồi, phiên bản của cache được sử dụng cho đến khi nó không còn tươi mới nữa, cái được xác định bằng thông số max-age hoặc expires, hoặc cho đến khi nó bị loại bỏ khỏi cache vì một lý do nào đó-chẳng hạn, người dùng xóa bộ nhớ đệm cache của trình duyệt. Hệ quả là, người dùng khác nhau, cuối cùng có thể sử dụng các phiên bản khác nhau của cùng một file khi trang được xây dựng: những người dùng vừa mới tìm nạp tài nguyên sử dụng phiên bản mới, trong khi người dùng cache bản sao từ trước (nhưng vẫn hợp lệ, ý là chưa hết hạn “max-age”) sử dụng một phiên bản cũ hơn của phản hồi.

Làm thế nào bạn hoàn thành được cả hai mục tiêu: cache phía máy khách (client-side) và cập nhật nhanh chóng? Bạn thay đổi URL của tài nguyên và ép người dùng tải phản hồi mới bất cứ khi nào nội dung của nó thay đổi. Thường thì bạn làm điều đó bằng cách nhúng một fingerprint vào file, hoặc một con số chỉ phiên bản (vesion number), trong filename của nó-lấy ví dụ, style.x234dff.css.

hệ thống cache

Khả năng định nghĩa chính sách cache cho từng tài nguyên cho phép bạn xác định “hệ thống phân cấp cache / cache hierarchies” từ đó cho bạn quyền không chỉ xác định độ dài của từng cache, mà còn mức độ nhanh chóng người dùng nhìn thấy phiên bản mới. Để minh họa điều này, hãy phân tích ví dụ trên:

  • HTML được đánh dấu là “no-cache”, điều đó có nghĩa là trình duyệt lúc nào cũng xác nhận lại (revalidate) tài liệu cho từng yêu cầu và tìm nạp phiên bản mới nhất nếu nội dung thay đổi. Cũng vậy, trong mã đánh dấu HTML, bạn nhúng fingerprint vào URL cho tài nguyên CSS và JavaScript: nếu nội dung của các file này thay đổi, thì HTML của trang cũng thay đổi theo và một bản copy mới cho phản hồi HTML sẽ được tải về.
  • CSS cho phép cache bởi trình duyệt và các nền tảng cache trung gian (ví dụ CDN), và thiết lập thời gian hết hạn trong vòng 1 năm. Lưu ý rằng bạn có thể sử dụng “thời điểm trong tương lai xa” trong vòng 1 năm một cách an toàn bởi vì bạn đã nhúng file fingerprint vào bên trong tên file của nó: nếu CSS được cập nhật, URL cũng thay đổi.
  • JavaScript cũng được thiết lập thời gian hết hạn trong vòng 1 năm, nhưng được đánh dấu là private, có lẽ vì nó bao gồm một số dữ liệu riêng tư mà CDN không thể cache.
  • Ảnh được cache mà không cần phiên bản hoặc dấu nhận biết riêng biệt duy nhất (fingerprint) và được thiết lập thời gian hết hạn trong vòng 1 ngày.

Sự kết hợp giữa ETag, Cache-Control, và URL duy nhất cho phép bạn phân phối nội dung theo cách tốt nhất trên nhiều khía cạnh: thời gian hết hạn dài, quyết định phản hồi nào có thể được cache, và các cập nhật theo nhu cầu.

Danh sách kiểm tra cache

Không có chính sách cache nào là tốt nhất trong mọi trường hợp. Phụ thuộc vào mẫu lưu lượng truy cập (traffic patterns), kiểu dữ liệu (type of data), và các yêu cầu ứng dụng cụ thể cho mức độ tươi mới của dữ liệu (data freshness), bạn phải xác định và tùy chỉnh các cài đặt thích hợp với từng tài nguyên, cũng như “hệ thống cấp bậc caching” nói chung.

Một số mẹo và kỹ thuật để cần ghi nhớ khi bạn thực thi chiến lược cache:

  • Sử dụng URL cố định: nếu bạn phục vụ cùng nội dung trên các URL khác nhau, thế thì nội dung sẽ được tìm nạp và lưu trữ nhiều lần. Lưu ý: URL có phân biệt chữ hoa và chữ thường (case sensitive).
  • Đảm bảo máy chủ cung cấp token xác thực (ETag): các token xác thực loại bỏ nhu cầu tải dữ liệu mạng khi tài nguyên trên máy chủ không thay đổi.
  • Xác định các tài nguyên nào có thể được cache bởi trung gian: những tài nguyên có đáp ứng giống hệt nhau cho tất cả người dùng là ứng cử viên sáng giá để cache bởi CDN và các nền tảng trung gian khác.
  • Xác định thời gian sống tối ưu của cache cho từng nguồn tài nguyên: các nguồn khác nhau có thể yêu cầu mức độ tươi mới khác nhau. Kiểm tra và xác định max-age thích hợp cho từng cái.
  • Xác định hệ thống cấp bậc cache tốt nhất cho trang của bạn: kết hợp URL tài nguyên với fingerprint của nội dung và thời gian sống của cache ngắn hoặc no-cache cho các tài liệu HTML cho phép bạn làm chủ mức độ nhanh chóng khách hàng nhận được các cập nhật.
  • Tối thiểu hóa tải về bằng tách mã: một số nguồn tài nguyên cập nhật với tần số cao hơn các nguồn khác. Nếu có một nguồn tài nguyên cụ thể nào đó (chẳng hạn hàm JavaScript hoặc một thiết lập của CSS) thường cập nhật, hãy xem xét phân phối mã bằng một file riêng biệt. Làm thế cho phép phần còn lại của nội dung (ví dụ, các thư viện mã ít khi thay đổi) được tìm nạp từ cache và tối thiểu hóa số lượng nội dung tải về bất cứ khi nào cập nhật đã được tìm nạp.

(Dịch từ bài viết HTTP Caching, tác giả: Ilya Grigorik, trang Google Developers)

Leave a Comment