Mengoptimalkan aplikasi fullstack dari ujung ke ujung: lapisan caching (CDN, ISR, Redis), strategi cache untuk query database, dan praktik menjaga Core Web Vitals — LCP, INP, dan CLS — tetap hijau.

"Kenapa aplikasi saya lambat?" adalah pertanyaan yang hampir selalu bisa dijawab dengan satu kata: duplikasi kerja. Server menghitung ulang, database meng-query ulang, dan browser men-download ulang hal yang sebenarnya sama dari waktu ke waktu. Episode ini membahas caching & performance — menghapus duplikasi kerja itu di setiap lapisan.
Caching adalah topik fullstack murni: ia hidup di CDN (jaringan), ISR (framework), Redis (server), dan browser (client). Kalian tidak bisa optimal hanya dengan satu lapisan — harus semua.
Setiap lapisan menangkap jenis kerja yang berbeda:
| Lapisan | Menyimpan | Contoh |
|---|---|---|
| CDN | Respons HTTP statis | HTML, gambar, JS/CSS |
| Framework (ISR) | Halaman render | /notes, /blog/[slug] |
| Redis | Hasil query / objek | daftar note, profil user |
| Browser | Aset HTTP | file JS/CSS (cache-control) |
Sebelum mengoptimasi, tahu dulu apa yang diukur. Core Web Vitals adalah tiga metrik yang dinilai Google dan pengguna:
| Metrik | Mengukur | Target |
|---|---|---|
| LCP (Largest Contentful Paint) | Kecepatan konten utama tampil | ≤ 2,5 detik |
| INP (Interaction to Next Paint) | Responsivitas interaksi | ≤ 200 ms |
| CLS (Cumulative Layout Shift) | Lompatan tata letak | ≤ 0,1 |
LCP adalah metrik paling relevan untuk fullstack: ia diukur dari jaringan sampai render konten utama. CLS paling sering disabotase oleh gambar tanpa dimensi atau konten yang muncul setelah layout — hindari itu sejak awal.
Note
Aturan LCP yang sering dilupakan: konten utama halaman seharusnya datang dari HTML awal, bukan menunggu JavaScript di client. Inilah alasan Server Components (episode 3) dan ISR — HTML jadi dikirim dari server/CDN, browser tidak menunggu JS bundel untuk menampilkan isi.
ISR (Incremental Static Regeneration) merender halaman sebagai statis di build, lalu memperbaruinya di background sesuai jadwal — sudah kita singgung di episode 8 dengan revalidate. Contoh halaman publik yang jarang berubah:
export const revalidate = 300 // 5 menit
export default async function Page() {
const notes = await prisma.note.findMany({
where: { published: true },
orderBy: { createdAt: "desc" },
})
return <NoteList notes={notes} />
}Halaman ini di-render sekali, disajikan dari cache selama 5 menit ke semua pengguna, lalu di-render ulang di background. Database tidak disentuh per request — beban turun drastis tanpa mengorbankan kesegaran yang berarti.
Untuk data dinamis yang tidak bisa di-static (misal daftar per user), cache di Redis menangkap query berulang:
import { Redis } from "ioredis"
const redis = new Redis({ host: "localhost", port: 6379 })
export async function cacheGet<T>(key: string, fn: () => Promise<T>, ttl = 60) {
const cached = await redis.get(key)
if (cached) return JSON.parse(cached) as T
const value = await fn()
await redis.set(key, JSON.stringify(value), "EX", ttl)
return value
}const notes = await cacheGet("notes:public", () =>
prisma.note.findMany({ where: { published: true } }),
)Invalidasi adalah bagian tersulit cache: saat data berubah, cache harus dihapus atau update:
"use server"
import { redis } from "@/lib/cache"
export async function createNote(formData: FormData) {
const note = await prisma.note.create({ data: {...} })
await redis.del("notes:public") // cache basi dihapus
revalidatePath("/notes")
}Warning
Pola "cache-then-invalidate" di atas memakai cache-aside (baca cache, miss → isi cache). Kesalahan umum: lupa menghapus cache di semua jalur tulis. Buat daftar key cache dan invalidate di satu tempat — jangan tersebar di banyak server action.
Gambar adalah penyebab CLS paling umum dan konstributor LCP terbesar. Di Next.js, gunakan next/image — ia menangani responsive sizing, format modern (WebP/AVIF), dan lazy load secara bawaan:
import Image from "next/image"
<Image
src="/static/note-cover.png"
alt="Sampul catatan"
width={800}
height={450}
priority
/>Dua aturan emas: selalu isi width & height (mencegah CLS) dan priority hanya untuk gambar LCP (gambar di atas fold) — sisanya lazy load.
Selesaikan dengan urutan berikut:
/notes dengan Lighthouse di Chrome DevTools — catat LCP/INP/CLS awal.revalidate = 300) untuk halaman publik.next/image dengan width & height.curl -s -o /dev/null -w "%{time_total}s\n" http://localhost:3000/notesInti yang harus dibawa pulang:
next/image dengan width/height mencegah CLS dan mempercepat LCP.Di episode 16 selanjutnya kita membahas security fullstack — OWASP, XSS, CSRF, injection, CSP, dan secrets — untuk mengamankan aplikasi dari serangan yang paling umum. Sampai jumpa di episode 16!