Membedah caching dan performa Rails: fragment & page caching, Russian Doll caching, low-level cache, cache stores (Redis & Solid Cache), dan optimasi query dengan includes/joins untuk membasmi N+1 queries

Setelah di episode 11 kalian mempercepat response dengan background job, episode 12 membahas cara lain yang sering jauh lebih berdampak: caching dan optimasi query. Sebelum menambah server, Rails menawarkan berlapis-lapis cache yang bisa membuat halaman loading dari ratusan milidetik menjadi belasan.
Mengapa episode ini penting? Karena performa bukan fitur tambahan — ia pengalaman pengguna dan biaya infrastruktur. Halaman yang lambat berarti user pergi dan biaya server naik. Episode ini mengajarkan hierarki cache Rails, dari yang paling kasar (full page) sampai paling halus (nilai tunggal), plus teknik query yang menyembuhkan penyebab lambat paling umum: N+1 queries.
Semakin ke bawah, semakin halus granularitasnya dan semakin banyak kontrol yang kalian miliki. Yang paling sering memberi ROI terbesar: fragment cache + query optimization.
Fragment cache menyimpan potongan HTML yang mahal untuk dirender. Halaman post yang menampilkan komentar adalah kandidat sempurna:
<% cache @post do %>
<h1><%= @post.title %></h1>
<p><%= @post.body %></p>
<section id="comments">
<%= render @post.comments %>
</section>
<% end %>cache @post menyimpan HTML dengan key berdasarkan @post — saat post diubah, key berubah dan cache dianggap stale. Rails memakai cache_key model (posts/1-20260816120000) sebagai basis.
Nested cache — cache luar memakai key yang berisi key cache dalam — disebut Russian Doll:
<% cache [@post, "index"] do %>
<%= render @post.comments %>
<% end %><% cache comment do %>
<p><%= comment.body %></p>
<small>oleh <%= comment.user.name %></small>
<% end %>Saat satu komentar berubah, hanya fragment komentar itu yang invalid — fragment post luar tetap dipakai karena cache_key-nya tidak berubah. Ini kunci efisiensi Russian Doll.
Tip
Cek apakah fragment cache benar-benar terpakai dengan bin/rails dev:cache (memaksa cache aktif di development) lalu lihat log: baris Read fragment vs Write fragment menunjukkan hit rate.
Untuk nilai tunggal — bukan HTML — gunakan Rails.cache langsung:
Rails.cache.fetch("user_#{user.id}_post_count", expires_in: 10.minutes) do
user.posts.count
endfetch memakai cache jika ada, menghitung jika tidak, dan menyimpannya. Contoh kasus: hitungan, kalkulasi mahal, atau hasil aggregasi yang jarang berubah.
Rails 8 memakai Solid Cache secara default — store berbasis database, tanpa Redis:
config.cache_store = :solid_cache_storeAlternatif: Redis (gem redis) untuk cache yang sangat cepat dan shared antar instance:
config.cache_store = :redis_cache_store, { url: "redis://localhost:6379/1" }| Store | Kelebihan | Kekurangan |
|---|---|---|
| Solid Cache (DB) | Tanpa infra tambahan, transactional | Bergantung kinerja DB |
| Redis | Sangat cepat, TTL fleksibel | Butuh infra Redis |
| MemoryStore | Tercepat, untuk dev | Tidak shared antar proses |
Di production multi-instance, cache harus shared (Redis/Solid Cache di database yang sama) — MemoryStore di tiap instance membuat fragment cache tidak konsisten.
Sebelum render, Rails bisa menghemat dengan HTTP cache headers:
def show
@post = Post.find(params[:id])
fresh_when @post
endfresh_when membandingkan If-None-Match/If-Modified-Since — jika data belum berubah, Rails mengirim 304 Not Modified tanpa merender view sama sekali. Ini cache paling efisien karena tidak menyentuh server render.
N+1 adalah penyebab lambat paling umum: untuk 100 post, aplikasi menjalankan 1 query untuk posts + 100 query untuk user masing-masing. Gejalanya: SELECT posts lalu puluhan SELECT users WHERE id = ... di log.
<% @posts.each do |post| %>
<p><%= post.user.name %></p> <!-- query user tiap iterasi! -->
<% end %>@posts = Post.includes(:user).limit(50)includes memuat semua user dalam satu query tambahan (WHERE id IN (...)). Total: 2 query untuk 50 post — bukan 51.
joins dipakai saat kalian memfilter berdasarkan relasi, bukan memuat data:
Post.joins(:user).where(users: { admin: true })Perbedaannya: includes = eager loading (hindari N+1), joins = INNER JOIN untuk penyaringan. Untuk filter + eager load sekaligus, gabungkan keduanya.
Post.joins(:comments).group(:id).countSatu query untuk semua post, bukan loop post.comments.count. Pattern ini penting untuk dashboard/reporting (diperdalam di episode 21).
Jangan menebak. Ukur dulu:
bin/rails runner '
ActiveRecord::Base.logger = Logger.new(STDOUT)
Post.includes(:user).limit(50).to_a
'Dan aktifkan deteksi N+1 dengan gem bullet di development — ia menandai query N+1, penggunaan includes yang tidak perlu, dan counter cache yang belum dipakai. Optimasi tanpa data hanyalah tebakan.
Warning
Cache menyimpan hasil render — pastikan data yang di-cache tidak sensitif (misal konten milik user tertentu yang bisa dilihat user lain). Fragment cache yang salah scope bisa menjadi celah kebocoran data; selalu sertakan identitas pengguna pada key bila kontennya personal.
each lalu memanggil relasi di dalamnya — N+1 klasik; pakai includes.post.comments.count di loop; gunakan group atau counter cache.Episode 12 membekali kalian caching Rails: hierarki dari HTTP caching (304) hingga low-level cache, fragment cache dan Russian Doll untuk HTML mahal, cache stores (Solid Cache database-driven vs Redis), serta optimasi query dengan includes/joins/group untuk membasmi N+1.
Inti yang harus dibawa pulang:
Rails.cache.fetch) untuk nilai mahal yang jarang berubah.includes untuk eager loading, joins untuk filter, group untuk agregasi.Di episode 13 selanjutnya kita akan membedah Active Storage & file upload — attachments, image variants, storage services (lokal & S3), dan direct upload. Sampai jumpa di episode 13!