Mempercepat aplikasi dengan caching berlapis: PSR-6/PSR-16 cache pools dengan adapter Redis & APCu, HTTP cache via Cache-Control dan ESI, optimalisasi opcache, serta strategi invalidasi cache yang benar agar data tidak basi.

Setelah API Platform di episode 13, pertanyaan berikutnya yang selalu muncul di produksi: kenapa lambat, dan bagaimana mempercepatnya? Episode 14 menjawab dengan caching berlapis — menyimpan hasil kerja yang mahal agar tidak diulang.
Mengapa caching sulit? Karena bukan hanya soal menyimpan — tapi soal kapan harus membuang. Cache yang basi (stale) lebih buruk dari tidak ada cache: pengguna melihat data lama tanpa sadar. Episode ini mengajarkan tidak hanya bagaimana cache, tapi strategi invalidasi yang benar — kemampuan yang membedakan aplikasi yang sekadar "memakai cache" dan yang "mengelola cache".
Pikirkan cache sebagai empat lapisan, dari yang terdekat ke pengguna:
| Lapisan | Teknologi | Yang di-cache |
|---|---|---|
| Browser | Cache-Control header | Asset & halaman utuh |
| Proxy/CDN | HTTP cache | Response publik |
| Aplikasi | Symfony Cache (PSR-6/16) | Query result, konfigurasi, render parsial |
| Database | Query cache bawaan DB | Hasil query SQL |
Episode ini fokus pada dua yang dipegang langsung aplikasi: Cache component (di dalam PHP) dan HTTP cache (via header).
Install component cache:
composer require symfony/cacheSymfony mengikuti dua standar:
CacheItemPoolInterface: API dengan item object (getItem(), save()).CacheInterface / SimpleCache: API singkat (get(), set()).Contoh memakai SimpleCache (paling sering dipakai):
use Psr\SimpleCache\CacheInterface;
public function articleCount(CacheInterface $cache): int
{
$count = $cache->get('article_count');
if ($count === null) {
$count = $this->articleRepository->count([]);
$cache->set('article_count', $count, 3600);
}
return $count;
}'article_count' disimpan 3600 detik, dan dipakai ulang tanpa menyentuh database. Pool default cache.app tersedia; pool lain dibuat di konfigurasi.
Pool diidentifikasi dengan adapter — backend penyimpanan:
framework:
cache:
pools:
app.result_pool:
adapter: cache.adapter.redis
default_lifetime: 600
app.fast_pool:
adapter: cache.adapter.apcu
default_lifetime: 60| Adapter | Kecepatan | Persistensi | Cocok untuk |
|---|---|---|---|
cache.adapter.filesystem | Sedang | Disk | Dev, single node |
cache.adapter.apcu | Sangat cepat | Per-proses | Data transien, per-server |
cache.adapter.redis | Cepat | Terpusat | Multi-server, shared cache |
Aturan praktis: APCu untuk data yang boleh basi per-server (konfigurasi, metadata); Redis untuk data yang harus konsisten lintas server (session, rate limit, shared result). Redis akan berperan besar saat scaling di episode 24.
Cache paling murah adalah yang tidak pernah sampai ke aplikasi — didelegasikan ke proxy/browser via header. Set cache header pada response:
public function index(): Response
{
$response = $this->render('article/index.html.twig', [
'articles' => $this->articleRepository->findLatest(10),
]);
$response->setPublic();
$response->setMaxAge(60); // 60 detik di proxy
$response->setSharedMaxAge(60); // khusus untuk shared cache
return $response;
}Hasilnya: proxy/CDN menyimpan halaman 60 detik, permintaan kedua tidak menyentuh PHP sama sekali. Di development, kalian bisa melihat status cache dengan header respons.
Invalidasi HTTP cache tidak se-trivial cache aplikasi: karena proxy tidak tahu perubahan database, gunakan cache tags (untuk reverse proxy yang mendukungnya) atau kurangi maxAge — atau pakai pendekatan ESI untuk fragment cache.
Banyak halaman punya bagian yang berubah cepat (keranjang, counter) di tengah halaman yang lambat berubah. Edge Side Includes (ESI) memungkinkan proxy menyimpan bagian halaman:
framework:
esi: true
fragments: { path: /_fragment }<div id="cart">
{{ render_esi(controller('App\\Controller\\CartController::summary')) }}
</div>render_esi() meminta proxy merender fragment secara terpisah — halaman induk di-cache lama, fragment cart di-cache singkat. Ini strategi klasik untuk portal dan e-commerce.
Jangan lupa lapisan paling dasar: OPCache meng-compile PHP bytecode dan menyimpannya di memory, menghemat kompilasi ulang setiap request. Ini wajib aktif di production:
opcache.enable=1
opcache.enable_cli=1
opcache.memory_consumption=256
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0validate_timestamps=0 di production membuat opcache tidak mengecek file setiap request (perlu cache:clear/restart saat deploy). Tanpa opcache, semua optimasi lain terasa sia-sia — ini optimasi dengan rasio usaha/hasil terbaik.
Ini bagian yang paling sering salah. Tiga strategi inti:
Contoh strategi 2 dengan cache:pool:delete atau memakai cache dan hapus setelah flush:
use Psr\SimpleCache\CacheInterface;
public function save(Article $article, CacheInterface $cache): void
{
$this->em->persist($article);
$this->em->flush();
$cache->delete('article_count');
}Warning
Cache tags tersedia di pool berbasis Redis (cache.adapter.redis_tag_aware) dan didukung taggable cache. Jangan berasumsi semua adapter mendukung tags — pool filesystem & APCu tidak. Pastikan pilihan adapter kalian sesuai sebelum mengandalkan tags.
Beberapa perintah yang membantu operasional:
php bin/console cache:pool:list # daftar pool & ukuran
php bin/console cache:pool:clear # kosongkan semua pool
php bin/console cache:pool:delete cache.app article_count
php bin/console cache:clear # bersihkan cache frameworkIngat: cache:clear membersihkan cache framework (route, container, twig), sedangkan cache:pool:clear membersihkan cache aplikasi (data yang kalian simpan). Keduanya sering tertukar saat debugging.
Pada episode 14 ini, kalian telah mempercepat aplikasi dengan cache berlapis.
Inti yang harus dibawa pulang:
CacheItemPoolInterface) dan PSR-16 (SimpleCache) — pakai SimpleCache untuk umum.Cache-Control; ESI untuk fragment; opcache wajib di produksi.Di episode 15 selanjutnya kita jadi detektif: Logging, Profiler & Debugging — Monolog channel & structured logging, Symfony Profiler & Web Debug Toolbar, var-dumper, dan teknik men-debug performa & error di production. Sampai jumpa di episode 15!