Nginx static file và cache trình duyệt: cấu hình thực tế cho website

Cấu hình Nginx static file và browser cache theo từng nhóm tài nguyên, có ví dụ Cache-Control, ETag, 304, sendfile, kiểm tra curl và rollback production.

NHT
· 7 phút đọc
Sơ đồ luồng Nginx phân phối static file và cache trình duyệt cho website

Nginx static file và cache trình duyệt là hai lớp tối ưu thường bị trộn lẫn. Nginx chịu trách nhiệm tìm và trả về các tài nguyên tĩnh như CSS, JavaScript, font và hình ảnh; còn trình duyệt quyết định có dùng lại response đã lưu hay phải hỏi lại server. Cấu hình đúng giúp giảm request lặp, giảm tải VPS và làm website phản hồi nhanh hơn mà không biến nội dung mới thành bản cache cũ.

Bài này trình bày một cấu hình thực tế cho website chạy trên Nginx, kèm cách kiểm tra bằng curl, cách chọn thời gian cache cho từng nhóm file và quy trình rollback khi cache gây lỗi. Ví dụ giả định Ubuntu/Debian, Nginx đã cài sẵn và website dùng document root /var/www/example.com/current/public.

Nginx static file và browser cache khác nhau thế nào?

Static file là file có sẵn trên đĩa và có thể trả trực tiếp, chẳng hạn style.css, app.js, logo.webp hoặc font. Nginx ánh xạ URL tới đường dẫn trên filesystem bằng root và xử lý request mà không cần gọi PHP-FPM hay ứng dụng backend. Tài liệu NGINX mô tả đây là mô hình phục vụ nội dung tĩnh theo root directory và location [1].

Browser cache là cơ chế lưu response ở phía client. Khi response còn fresh, trình duyệt có thể dùng lại mà không gửi request tới origin. Khi response stale, trình duyệt có thể revalidate bằng validator như ETag hoặc Last-Modified, và server có thể trả 304 Not Modified thay vì gửi lại toàn bộ file [2]. Vì vậy, tối ưu file tĩnh và tối ưu header cache là hai việc liên quan nhưng không đồng nhất.

Trước khi cache: xác định đúng nhóm tài nguyên

Không nên đặt một chính sách duy nhất cho mọi URL. CSS, JavaScript, font và ảnh có tên file chứa hash hoặc version thường an toàn để cache dài ngày. HTML, API response, trang đăng nhập, giỏ hàng và nội dung phụ thuộc cookie cần chính sách ngắn hơn hoặc không cache. MDN phân biệt private cache của trình duyệt với shared cache và lưu ý rằng nội dung cá nhân hóa không nên bị lưu ở nơi dùng chung [2].

Nhóm tài nguyênChiến lược gợi ýLý do
CSS/JS có hash, ví dụ app.8f31c.jspublic, max-age=31536000, immutableĐổi nội dung bằng tên file mới
Ảnh, font và icon tĩnh7–30 ngày; có thể dài hơn nếu đã version hóaGiảm request lặp nhưng vẫn dễ cập nhật
HTML trang bài viếtKhông cache dài ở browser; cân nhắc revalidateNội dung có thể thay đổi và cần SEO cập nhật
API hoặc response có cookieprivate hoặc no-storeTránh lộ dữ liệu cá nhân qua cache dùng chung

Cấu hình location cho static file

Ví dụ sau chỉ áp dụng cho asset tĩnh. Không đặt block này lên toàn bộ website nếu cùng server block còn xử lý PHP, proxy hoặc route động.

server {
    listen 443 ssl http2;
    server_name example.com www.example.com;

    root /var/www/example.com/current/public;
    index index.html index.php;

    location /assets/ {
        try_files $uri =404;
        access_log off;
        log_not_found off;
    }

    location ~* \.(?:css|js|mjs|jpg|jpeg|png|gif|webp|svg|ico|woff2?)$ {
        try_files $uri =404;
        access_log off;
    }
}

try_files $uri =404 giúp tránh chuyển một file không tồn tại sang fallback động ngoài ý muốn. Tuy nhiên, regex location cần được kiểm tra cùng toàn bộ cấu hình hiện có; một location viết quá rộng có thể làm hỏng route của ứng dụng. Nếu website dùng framework có front controller, hãy giữ nguyên block PHP/proxy hiện tại và chỉ thêm policy cho các thư mục asset đã xác định.

Thêm Cache-Control và Expires theo cách an toàn

Cache-Control: max-age là cách hiện đại để mô tả thời gian freshness. Header Expires thuộc HTTP/1.0 và được giữ chủ yếu vì tương thích; khi cả hai cùng xuất hiện, max-age được ưu tiên [2]. Trong Nginx, có thể dùng expires để sinh Expires và Cache-Control, hoặc dùng add_header khi cần directive cụ thể hơn.

location ~* \.(?:css|js|mjs|jpg|jpeg|png|gif|webp|svg|ico|woff2?)$ {
    try_files $uri =404;
    add_header Cache-Control "public, max-age=2592000" always;
    access_log off;
}

# Chỉ dùng immutable khi URL đã có hash/version
location ~* \.(?:css|js|mjs)$ {
    try_files $uri =404;
    add_header Cache-Control "public, max-age=31536000, immutable" always;
}

Hai block regex trên có thể tranh quyền match tùy cấu hình. Trong production, nên gom chúng thành các nhóm rõ ràng hoặc dùng location theo thư mục để tránh áp dụng nhầm chính sách một năm cho file không version hóa. Một phương án dễ vận hành là build frontend ra thư mục /assets/, mọi file trong đó có hash, còn HTML nằm ngoài policy cache dài.

ETag, Last-Modified và status 304

Cache dài không phải lựa chọn duy nhất. Với tài nguyên có thể thay đổi nhưng vẫn muốn giảm băng thông, validator cho phép client hỏi lại server và nhận phản hồi ngắn. Nginx thường có thể phát Last-Modified từ mtime của file và dùng ETag theo metadata file tùy phiên bản/cấu hình. Kiểm tra thực tế thay vì đoán, bởi proxy phía trước hoặc CDN có thể sửa header.

curl -sSI https://example.com/assets/app.css
curl -sSI -H 'If-None-Match: "etag-da-luu"' https://example.com/assets/app.css
curl -sSI -H 'If-Modified-Since: Wed, 01 Jan 2025 00:00:00 GMT' https://example.com/assets/app.css

Kỳ vọng thường là response đầu tiên có Cache-Control, ETag hoặc Last-Modified. Request revalidation có thể trả 304 Not Modified. Nếu luôn nhận 200, hãy xem lại validator, header bị proxy loại bỏ, hoặc file đang được tạo lại với mtime mới trong mỗi lần deploy.

sendfile và các tối ưu phục vụ file

NGINX documentation có đề cập sendfile, tcp_nopushtcp_nodelay như các hướng tối ưu phục vụ nội dung [1]. Cấu hình cơ bản có thể đặt trong block http:

http {
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;

    include /etc/nginx/mime.types;
    default_type application/octet-stream;
}

Đây không phải công thức tăng tốc tuyệt đối cho mọi workload. Hãy đo TTFB, kích thước response, cache hit và thời gian tải thực tế trước/sau thay đổi. Không bật tùy tiện các directive mà không biết chúng tương tác thế nào với TLS termination, CDN hoặc nền tảng hosting.

Cache asset có query string

Với nhiều cấu hình, URL /app.css?v=2 vẫn được xem là cùng một file trên filesystem, nhưng cache key ở browser/CDN có thể bao gồm query string. Nếu dùng query string làm version, hãy bảo đảm pipeline deploy luôn tăng version khi nội dung đổi. Nếu đã dùng filename hash, không cần thêm query string chỉ để “ép cache”.

Kiểm tra sau khi sửa cấu hình

Quy trình production nên có ba lớp kiểm tra. Trước hết, chạy sudo nginx -t để kiểm tra syntax và đường dẫn include. Tiếp theo, reload bằng sudo systemctl reload nginx thay vì restart để giảm gián đoạn. Cuối cùng, kiểm tra response từ domain công khai và kiểm tra file có tồn tại trên disk.

sudo nginx -t
sudo systemctl reload nginx
curl -sSI https://example.com/assets/app.css
curl -sSI https://example.com/assets/app.8f31c.js
sudo tail -n 50 /var/log/nginx/error.log

Trong DevTools, mở Network, tắt cache chỉ để debug rồi tải lại; khi đánh giá người dùng thực, hãy mở cửa sổ mới hoặc profile sạch. Kiểm tra cả response headers lẫn status 200/304. Đừng kết luận cache hoạt động chỉ vì trình duyệt hiện nội dung cũ.

Lỗi thường gặp và cách rollback

Hiện tượngNguyên nhân có thểCách xử lý
CSS cũ sau deployFilename không đổi nhưng max-age quá dàiĐổi sang filename hash hoặc giảm max-age; purge cache nếu có CDN
File tĩnh trả 404Sai root, quyền file hoặc location match saiKiểm tra nginx -T, filesystem path và permission
Trang cá nhân bị cacheÁp dụng public cho response có cookieĐưa route về private/no-store và purge cache
Header không xuất hiệnProxy/CDN ghi đè hoặc add_header thiếu alwaysKiểm tra từng lớp bằng curl -I

Rollback nên là thay file cấu hình bằng bản đã backup, chạy sudo nginx -t, reload và kiểm tra lại một asset đại diện. Không xóa cache hàng loạt trước khi biết cache nằm ở browser, reverse proxy hay CDN.

Checklist production

  • Đã xác định asset nào version hóa bằng hash và asset nào có thể thay đổi tại cùng URL.
  • Đã tách policy cho static asset khỏi HTML, API và nội dung cá nhân hóa.
  • Đã kiểm tra Cache-Control, ETag/Last-Modified, status 200/304 bằng curl.
  • Đã chạy nginx -t, reload an toàn và lưu bản cấu hình trước khi sửa.
  • Đã kiểm tra quyền file, MIME type, access log và error log.

Kết luận

Cấu hình Nginx static file hiệu quả không bắt đầu bằng việc đặt max-age=31536000 cho mọi thứ. Cách bền vững là phân loại tài nguyên, version hóa file có thể cache dài, dùng validator cho file thay đổi và xác minh response từ bên ngoài. Nếu bạn đang triển khai nhiều website trên cùng VPS, có thể tham khảo thêm bài cấu hình Nginx reverse proxy cho nhiều website trên cùng một VPS để tách server block và kiểm tra rollback theo từng domain.

Tham khảo

  1. NGINX Documentation — Serve Static Content.
  2. MDN Web Docs — HTTP caching.
  3. MDN Web Docs — Cache-Control header.