DEV Community

ahmet gedik
ahmet gedik

Posted on

Implementing File-Based Page Caching in PHP

Introduction

Caching is the single most impactful performance optimization for any PHP website. When I built ViralVidVault, file-based page caching reduced our average page load time by over 70%. Here's exactly how to implement it.

The Architecture

The concept is simple: render a page once, save the HTML to a file, and serve that file directly for subsequent requests until it expires. No database queries, no template rendering, no PHP processing beyond reading a file.

Request → Check cache file exists & not expired?
  ├─ YES → Read file → Send response (fast!)
  └─ NO  → Render page → Save to cache → Send response
Enter fullscreen mode Exit fullscreen mode

Implementation

The Cache Class

<?php

class PageCache
{
    private string $cacheDir;
    private array $ttlRules = [];
    private int $defaultTtl;

    public function __construct(string $cacheDir, int $defaultTtl = 10800)
    {
        $this->cacheDir = rtrim($cacheDir, '/');
        $this->defaultTtl = $defaultTtl;

        if (!is_dir($this->cacheDir)) {
            mkdir($this->cacheDir, 0755, true);
        }
    }

    public function setTTL(string $pattern, int $seconds): void
    {
        $this->ttlRules[$pattern] = $seconds;
    }

    public function serve(): bool
    {
        if ($_SERVER['REQUEST_METHOD'] !== 'GET') return false;

        $file = $this->getCacheFile();
        if (!file_exists($file)) return false;

        $ttl = $this->getTTL();
        $age = time() - filemtime($file);

        if ($age > $ttl) {
            unlink($file);
            return false;
        }

        // Send cache headers
        header('X-Cache: HIT');
        header('X-Cache-Age: ' . $age);
        header('Cache-Control: public, max-age=' . ($ttl - $age));

        readfile($file);
        return true;
    }

    public function store(string $html): void
    {
        $file = $this->getCacheFile();
        $dir = dirname($file);

        if (!is_dir($dir)) {
            mkdir($dir, 0755, true);
        }

        file_put_contents($file, $html, LOCK_EX);
    }

    public function clear(string $path = ''): void
    {
        if ($path) {
            $file = $this->cacheDir . '/' . md5($path) . '.html';
            if (file_exists($file)) unlink($file);
        } else {
            $files = glob($this->cacheDir . '/*.html');
            array_map('unlink', $files ?: []);
        }
    }

    private function getCacheFile(): string
    {
        $uri = $_SERVER['REQUEST_URI'] ?? '/';
        return $this->cacheDir . '/' . md5($uri) . '.html';
    }

    private function getTTL(): int
    {
        $uri = $_SERVER['REQUEST_URI'] ?? '/';

        foreach ($this->ttlRules as $pattern => $ttl) {
            if ($pattern === $uri || fnmatch($pattern, $uri)) {
                return $ttl;
            }
        }

        return $this->defaultTtl;
    }
}
Enter fullscreen mode Exit fullscreen mode

Integration in index.php

Here's how it plugs into a typical PHP entry point:

<?php
// public/index.php

$cache = new PageCache(__DIR__ . '/../data/pagecache');
$cache->setTTL('/', 10800);           // Home: 3h
$cache->setTTL('/category/*', 10800); // Categories: 3h
$cache->setTTL('/watch/*', 21600);    // Watch: 6h
$cache->setTTL('/search*', 600);      // Search: 10min

// Try cache first
if ($cache->serve()) {
    exit;
}

// Cache miss - render normally
ob_start();

// ... routing, controller, template rendering ...
require __DIR__ . '/../app/bootstrap.php';

$html = ob_get_flush();
$cache->store($html);
Enter fullscreen mode Exit fullscreen mode

Cache Invalidation

The hardest problem in computer science, right? In practice, for a content site, you invalidate in two places:

  1. After cron fetches new content — Clear home and category caches
  2. After admin actions — Clear specific page caches
// In your cron script, after fetching new videos:
$cache = new PageCache(__DIR__ . '/../data/pagecache');
$cache->clear('/');  // Home page
$cache->clear();     // Or clear everything after a big update
Enter fullscreen mode Exit fullscreen mode

Performance Results

On ViralVidVault, adding this middle cache layer produced significant gains:

Metric Before After
Avg page load 340ms 95ms
TTFB 280ms 45ms
DB queries/request 8-12 0 (cache hit)
PHP memory 14MB 4MB

Tips

  • Use LOCK_EX when writing to prevent partial reads during concurrent writes
  • Don't cache POST requests or pages with user-specific content
  • Add X-Cache headers for easy debugging
  • Run garbage collection periodically to clean up expired files
  • Use md5 of URI as filename to handle any URL safely

This exact pattern powers the cache layer at viralvidvault.com. It sits between LiteSpeed's built-in cache and the application's data cache, creating a 3-tier system that keeps the site snappy for users browsing viral videos from across Europe.


Part of the "Building ViralVidVault" series.

Top comments (0)