Làm chủ HTTP Caching trong Laravel với ETag & Last-Modified

· 5 min read

Trong một CMS dựa trên file như blog này, nội dung không thay đổi cho đến khi bạn push một commit. Điều này khiến nó trở thành ứng cử viên hoàn hảo cho các chiến lược HTTP caching mạnh mẽ. Mặc dù Laravel cung cấp các driver caching phía server tuyệt vời (Redis, Memcached), client-side caching thông qua HTTP header còn nhanh hơn vì nó ngăn trình duyệt thậm chí không cần tải xuống nội dung trang.

Hôm nay, chúng ta sẽ triển khai header ETagLast-Modified để hỗ trợ conditional request (304 Not Modified).

Tại sao cần Conditional Request?

Khi trình duyệt truy cập một trang đã xem trước đó, nó có thể gửi header để hỏi:

  • "File này có thay đổi kể từ [ngày] không?" (If-Modified-Since)
  • "File này có còn khớp với hash này không?" (If-None-Match)

Nếu câu trả lời là "Không, nó chưa thay đổi", server phản hồi với 304 Not Modified và body rỗng. Trình duyệt tải nội dung ngay lập tức từ cache local.

Chiến lược

Đối với Markdown blog, "sự thật" nằm ở file system.

  1. Last-Modified: Timestamp của filesystem (filemtime) từ file .md.
  2. ETag: Hash (md5) của nội dung file + file layout (tùy chọn nhưng được khuyến nghị).

Triển khai

Laravel làm điều này dễ dàng một cách đáng ngạc nhiên với method setCacheHeaders hoặc middleware.

1. Trong Controller

Đây là cách bạn có thể xử lý request cho một bài viết:

// App\Http\Controllers\PostController.php

public function show(string $slug, MarkdownPostService $service)
{
    $postFile = $service->getFilePath($slug);

    if (!file_exists($postFile)) {
        abort(404);
    }

    // 1. Lấy timestamp last modified của file markdown
    $lastModified = filemtime($postFile);
    
    // 2. Xác định ETag (hash của nội dung)
    $etag = md5_file($postFile);

    // 3. Kiểm tra xem cache của trình duyệt còn hợp lệ không
    // Method này tự động xử lý việc so sánh request header
    // với các giá trị mới tính toán của chúng ta.
    if (request()->headers->get('If-None-Match') === $etag ||
        strtotime(request()->headers->get('If-Modified-Since')) >= $lastModified) {
            return response(null, 304);
    }

    // 4. Nếu không phải 304, render toàn bộ trang
    $post = $service->find($slug);

    return response()
        ->view('blog.show', ['post' => $post])
        ->setEtag($etag)
        ->setLastModified(new \DateTime("@$lastModified"))
        ->setPublic()
        // Tùy chọn: đặt max-age/s-maxage
        ->setMaxAge(3600); 
}

2. Sử dụng Middleware

Nếu bạn muốn áp dụng điều này globally hoặc cho một nhóm route, middleware SetCacheHeaders tích hợp sẵn của Laravel rất mạnh mẽ, nhưng nó dựa vào các cặp key-value. Đối với việc kiểm tra file động, custom middleware thường sạch sẽ hơn.

Tạo App\Http\Middleware\SmartHttpCache.php:

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class SmartHttpCache
{
    public function handle(Request $request, Closure $next): Response
    {
        $response = $next($request);

        // Chỉ cache các request GET/HEAD thành công
        if (!$request->isMethodCacheable() || !$response->isSuccessful()) {
            return $response;
        }

        // Nếu response đã có ETag, giả định controller đã xử lý
        if ($response->getEtag()) {
            return $response;
        }

        // Tạo ETag từ nội dung response
        // Lưu ý: Đối với response lớn, hash toàn bộ body trong middleware 
        // nghĩa là chúng ta đã render view rồi. 
        // Cách tiếp cận Controller nhanh hơn (tránh render).
        $etag = md5($response->getContent());
        $response->setEtag($etag);
        
        // Method này so sánh Request header với Response header mới
        // và chuyển response thành 304 nếu chúng khớp nhau.
        $response->isNotModified($request);

        return $response;
    }
}

Tác động hiệu năng

Cách tiếp cận Controller vượt trội hơn cho flat-file blog.

  1. Request đến.
  2. Kiểm tra filesystem: filemtime() là một syscall O(1) cực kỳ nhanh.
  3. So sánh: Nếu header khớp, trả về 304.
  4. Tránh Parsing: Chúng ta bỏ qua hoàn toàn việc parse CommonMark, render Blade, và trích xuất Frontmatter. Đây là tiết kiệm CPU cực lớn.

Những điểm cần lưu ý

  1. Thay đổi Layout: Nếu bạn thay đổi layout blade (ví dụ: header/footer), file Markdown không thay đổi, nhưng output HTML đã thay đổi.
    • Giải pháp: Thêm mtime của file layout vào logic ETag.
  2. Môi trường: isNotModified() có thể hoạt động khác nếu bạn có global middleware sửa đổi header sau đó.

Kết luận

Bằng cách triển khai ETagLast-Modified, bạn biến ứng dụng Laravel động của mình thành thứ hoạt động gần như không thể phân biệt với static site generator (SSG) đối với khách truy cập quay lại. Đây là điều tốt nhất của cả hai thế giới: routing động với hiệu năng tĩnh.

Bình luận