Di antara semua header respons, Cache-Control berdampak paling besar terhadap kecepatan dan biaya aplikasi Anda: satu baris header menentukan apakah respons yang sama dikirim ulang dari server atau dilayani dari simpanan yang jauh lebih dekat ke pengguna. Tanpa header ini, perilaku cache menjadi tebak-tebakan — beberapa cache menyimpan seperlunya, beberapa lainnya menyimpan terlalu agresif, dan halaman pribadi bisa saja muncul di layar orang lain. Chapter ini membahas cara menyusun direktif Cache-Control dari PHP: direktif apa saja yang tersedia, siapa saja pihak yang menyimpan cache, cara membedakan konten yang boleh di-cache dari yang harus dilindungi, hubungannya dengan header Expires yang lebih tua, sampai header cache yang dipasang diam-diam oleh session_start().

Direktif Cache-Control dan Artinya #

Nilai Cache-Control adalah daftar direktif dipisah koma. Tabel berikut merangkum tujuh direktif yang paling sering Anda butuhkan:

DirektifEfekKapan dipakai
publicboleh disimpan oleh cache bersama (proxy, CDN)konten sama untuk semua pengunjung
privatehanya boleh disimpan cache browserhalaman per-pengguna yang tidak sensitif
no-cacheboleh disimpan, tetapi wajib revalidasi ke server sebelum dipakaikonten yang sering berubah
no-storedilarang menyimpan sama sekalidata sensitif: rekening, riwayat, token
max-age=Nrespons dianggap segar selama N detikkonten yang jarang berubah
s-maxage=Numur khusus cache bersama, menimpa max-ageTTL CDN berbeda dari TTL browser
must-revalidatesetelah kedaluwarsa, wajib cek ulang; dilarang memakai yang basidata yang tidak boleh usang

Menyusun baris header dari direktif tersebut adalah kerjaan fungsi murni yang mudah diuji:

php
<?php
function bangunCacheControl(array $directives): string
{
    $bagian = [];
    foreach ($directives as $nama => $nilai) {
        $bagian[] = is_bool($nilai) ? $nama : "$nama=$nilai";
    }
    return "Cache-Control: " . implode(", ", $bagian);
}

echo bangunCacheControl(["public" => true, "max-age" => 3600]) . "\n";
echo bangunCacheControl(["private" => true, "no-cache" => true]) . "\n";
echo bangunCacheControl(["no-store" => true]) . "\n";
echo bangunCacheControl(["public" => true, "max-age" => 600, "s-maxage" => 86400, "must-revalidate" => true]) . "\n";

Output yang Dihasilkan:

text
Cache-Control: public, max-age=3600
Cache-Control: private, no-cache
Cache-Control: no-store
Cache-Control: public, max-age=600, s-maxage=86400, must-revalidate

Konvensi arraynya sederhana namun praktis: direktif tanpa nilai (flag) dinyatakan dengan true, sedangkan direktif bernilai seperti max-age menyimpan angkanya. Baris keempat memperlihatkan kombinasi yang lazim untuk aset publik: browser boleh memakai salinannya selama 10 menit (max-age=600), CDN boleh menyimpan seharian (s-maxage=86400), dan begitu kedaluwarsa tidak ada tolerance untuk memakai salinan basi (must-revalidate).

Browser, Proxy, dan CDN #

Sebelum menyetel direktif, kita perlu tahu siapa saja yang menyimpan cache, karena direktif berbeda ditujukan kepada pihak berbeda. Ada tiga lapisan yang umum: cache browser milik satu pengguna di perangkatnya sendiri, proxy perusahaan atau penyedia internet yang melayani banyak pengguna sekaligus, dan CDN — jaringan server tersebar yang sengaja Anda sewa agar konten dekat secara geografis. Dua yang terakhir disebut shared cache karena salinannya bisa dipakai orang lain.

Inilah alasan direktif private dan public ada: halaman katalog boleh public karena isinya sama untuk semua orang, sedangkan halaman “pesanan saya” yang menampilkan nama pembeli wajib private — kalau tidak, proxy di jaringan kantor bisa menyajikan pesanan Budi kepada Siti. Sebaliknya, menandai semua halaman private membuat CDN tidak berdaya dan membebani server asal. Kombinasi max-age + s-maxage memberi kendali per lapisan seperti pada contoh sebelumnya.

Respons untuk Konten Statis dan Sensitif #

Keputusan caching sebaiknya dipusatkan di satu tempat — misalnya fungsi yang menerima deskripsi halaman lalu mengembalikan triple [status, headers, body] yang siap dikirim:

php
<?php
function bangunCacheControl(array $directives): string
{
    $bagian = [];
    foreach ($directives as $nama => $nilai) {
        $bagian[] = is_bool($nilai) ? $nama : "$nama=$nilai";
    }
    return "Cache-Control: " . implode(", ", $bagian);
}

function responsKonten(array $halaman): array
{
    if ($halaman["sensitif"]) {
        $cache = bangunCacheControl(["no-store" => true]);
    } elseif ($halaman["publik"]) {
        $cache = bangunCacheControl(["public" => true, "max-age" => $halaman["umur"]]);
    } else {
        $cache = bangunCacheControl(["private" => true, "no-cache" => true]);
    }

    return [200, [$cache, "Content-Type: {$halaman['tipe']}"], $halaman["isi"]];
}

$halamanStatis = [
    "sensitif" => false, "publik" => true, "umur" => 3600,
    "tipe" => "text/html; charset=UTF-8", "isi" => "<h1>Katalog Produk Toko Budi</h1>",
];
$halamanPribadi = [
    "sensitif" => true, "publik" => false, "umur" => 0,
    "tipe" => "text/html; charset=UTF-8", "isi" => "<h1>Riwayat pesanan Budi</h1>",
];

foreach (["katalog" => $halamanStatis, "riwayat" => $halamanPribadi] as $nama => $halaman) {
    [$status, $headers, $body] = responsKonten($halaman);
    echo "=== $nama (status $status) ===\n";
    foreach ($headers as $header) {
        echo "$header\n";
    }
    echo "body: $body\n";
}

Output yang Dihasilkan:

text
=== katalog (status 200) ===
Cache-Control: public, max-age=3600
Content-Type: text/html; charset=UTF-8
body: <h1>Katalog Produk Toko Budi</h1>
=== riwayat (status 200) ===
Cache-Control: no-store
Content-Type: text/html; charset=UTF-8
body: <h1>Riwayat pesanan Budi</h1>

Fungsi responsKonten() memilah tiga keadaan: halaman sensitif mendapat no-store total, halaman publik mendapat public plus umur, dan sisanya (per-pengguna namun tidak sensitif) mendapat private, no-cache — boleh disimpan browser demi kecepatan tampil ulang, tetapi selalu dicekkan ulang ke server agar tidak basi. Pola triple ini juga membuat seluruh kebijakan cache bisa diuji seperti fungsi biasa, tanpa server.

Cache-Control versus Expires #

Header Expires adalah mekanisme era HTTP/1.0: ia menyebut tanggal mutlak kapan respons kedaluwarsa. Cache-Control: max-age datang kemudian dengan satuan detik yang relatif. Ketika keduanya hadir, cache modern memakai max-age dan mengabaikan Expires:

php
<?php
$sekarang = new DateTimeImmutable("2026-01-15 08:00:00", new DateTimeZone("Asia/Jakarta"));
$jamKeDepan = $sekarang->modify("+1 hour")->setTimezone(new DateTimeZone("UTC"));

echo "Cache-Control: public, max-age=3600\n";
echo "Expires: " . $jamKeDepan->format("D, d M Y H:i:s") . " GMT\n";

Output yang Dihasilkan:

text
Cache-Control: public, max-age=3600
Expires: Thu, 15 Jan 2026 02:00:00 GMT

Padanan keduanya terlihat dari angka yang sama: max-age=3600 setara dengan Expires satu jam setelah respons dibuat (waktu beku dipakai agar contoh deterministik). Pendekatan modern cukup mengirim Cache-Control saja — tetapi klien-klien lawas yang tidak mengenalnya masih mengandalkan Expires, sehingga strategi kompatibel adalah mengirim keduanya. Kelemahan Expires perlu diwaspadai: tanggal mutlak bergantung pada ketepatan jam server dan klien; jam komputer pengguna yang melenceng bisa membuat cache kedaluwarsa terlalu cepat atau terlalu lama.

Session PHP dan Cache-Control #

Ada satu pemain yang memasang header cache tanpa Anda minta: session_start(). PHP mengikuti direktif session.cache_limiter untuk menyetel header cache pada setiap respons bersistema:

php
<?php
echo "session.cache_limiter bawaan: " . ini_get("session.cache_limiter") . "\n";
echo "session.cache_expire bawaan : " . ini_get("session.cache_expire") . " menit\n";

Output yang Dihasilkan:

text
session.cache_limiter bawaan: nocache
session.cache_expire bawaan : 180 menit

Nilai bawaan nocache membuat setiap halaman bersistasi mengirim Cache-Control: no-store, no-cache, must-revalidate — pilihan konservatif yang aman untuk halaman login dan data pribadi. Mari kita buktikan di server sungguhan:

php
<?php
// laporan-pribadi.php — dari folder berkas ini, jalankan:
//   php -S 127.0.0.1:8091
// lalu amati header responsnya:
//   curl -i http://127.0.0.1:8091/laporan-pribadi.php
session_start();

echo "Riwayat pesanan Budi\n";

Respons curl -i memperlihatkan deretan header yang dipasang PHP: Expires: Thu, 19 Nov 1981 08:52:00 GMT (tanggal kelahiran PHP versi pertama — sengaja di masa lalu agar selalu dianggap kedaluwarsa), Cache-Control: no-store, no-cache, must-revalidate, dan Pragma: no-cache untuk klien lebih tua lagi, di samping Set-Cookie sesi yang dibahas chapter sebelumnya. Limiter lain tersedia bila kebutuhan berbeda: public mengirim Cache-Control: public, max-age=10800 (nilai dari session.cache_expire 180 menit × 60 detik), private membatasi ke cache browser, dan string kosong mematikan header otomatis. Untuk API yang mengatur Cache-Control sendiri, limiter kosong adalah pilihan yang benar agar header tidak saling menimpa. Matikan server setelah selesai dengan Ctrl+C atau pkill -f "php -S".

Vary: Satu Respons Banyak Versi #

Satu URL belum tentu berarti satu isi — respons yang sama bisa berbeda tergantung header permintaan. Header Vary memberi tahu cache bahwa salinan hanya boleh dipakai bila header permintaan tersebut juga sama. Kasus paling umum adalah Vary: Accept-Encoding: versi terkompresi dan versi polos dari satu URL adalah dua representasi berbeda, dan cache yang tidak diberi tahu akan kesulitan menyajikan versi terkompresi kepada klien yang tidak mengerti gzip. Pembahasan lengkapnya menyusul di chapter response compression ; untuk halaman yang isinya bergantung pada bahasa, Vary: Accept-Language bekerja dengan cara yang sama.

Ringkasan #

Cache-Control mengendalikan tiga lapis cache — browser, proxy, CDN — lewat direktif: public/private memilih lapisan, max-age/s-maxage mengatur umur per lapisan, no-cache mewajibkan revalidasi, dan no-store melarang penyimpanan untuk data sensitif. Cache-Control mengalahkan Expires saat keduanya hadir, sementara session_start() diam-diam memasang header no-store, no-cache, must-revalidate lewat session.cache_limiter. Direktif no-cache menuntut mekanisme revalidasi agar berfungsi — dan revalidasi itulah yang dibangun oleh ETag, topik chapter berikutnya.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

contoh.php

Unduh berkas
php
<?php
// contoh.php — ringkasan penyusunan header Cache-Control
// dan kebijakan cache per jenis halaman.

function bangunCacheControl(array $directives): string
{
    $bagian = [];
    foreach ($directives as $nama => $nilai) {
        $bagian[] = is_bool($nilai) ? $nama : "$nama=$nilai";
    }
    return "Cache-Control: " . implode(", ", $bagian);
}

function responsKonten(array $halaman): array
{
    if ($halaman["sensitif"]) {
        $cache = bangunCacheControl(["no-store" => true]);
    } elseif ($halaman["publik"]) {
        $cache = bangunCacheControl(["public" => true, "max-age" => $halaman["umur"]]);
    } else {
        $cache = bangunCacheControl(["private" => true, "no-cache" => true]);
    }

    return [200, [$cache, "Content-Type: {$halaman['tipe']}"], $halaman["isi"]];
}

$halamanStatis = [
    "sensitif" => false, "publik" => true, "umur" => 3600,
    "tipe" => "text/html; charset=UTF-8", "isi" => "<h1>Katalog Produk Toko Budi</h1>",
];
$halamanPribadi = [
    "sensitif" => true, "publik" => false, "umur" => 0,
    "tipe" => "text/html; charset=UTF-8", "isi" => "<h1>Riwayat pesanan Budi</h1>",
];

foreach (["katalog" => $halamanStatis, "riwayat" => $halamanPribadi] as $nama => $halaman) {
    [$status, $headers, $body] = responsKonten($halaman);
    echo "=== $nama (status $status) ===\n";
    foreach ($headers as $header) {
        echo "$header\n";
    }
    echo "body: $body\n";
}

echo "=== Aset publik: TTL browser dan CDN terpisah ===\n";
echo bangunCacheControl(["public" => true, "max-age" => 600, "s-maxage" => 86400, "must-revalidate" => true]) . "\n";

echo "=== Padanan Expires untuk max-age=3600 (waktu beku) ===\n";
$sekarang = new DateTimeImmutable("2026-01-15 08:00:00", new DateTimeZone("Asia/Jakarta"));
$jamKeDepan = $sekarang->modify("+1 hour")->setTimezone(new DateTimeZone("UTC"));
echo bangunCacheControl(["public" => true, "max-age" => 3600]) . "\n";
echo "Expires: " . $jamKeDepan->format("D, d M Y H:i:s") . " GMT\n";