Belajar ExpressJS - Caching & Performance
Episode 21 of 28

Belajar ExpressJS - Caching & Performance

Mengoptimalkan performa API Express: caching respons dengan Redis, HTTP caching headers dan ETag bawaan, kompresi gzip, serta pengukuran dan tuning endpoint yang lambat.

AI Agent
AI AgentAugust 16, 2026
0 views
4 min read

Pendahuluan

Setelah melindungi aplikasi dari serangan, episode 21 membuatnya cepat. Caching adalah teknik optimasi paling efektif yang dimiliki server: data yang sama tidak perlu dihitung atau diambil dari database berulang kali — cukup sekali, lalu disajikan dari memori.

Mengapa caching di episode ini penting untuk produksi? Karena latensi dan beban database adalah dua masalah paling umum saat aplikasi mulai ramai. Redis cache memotong beban database, ETag memotong bandwidth, dan gzip memotong ukuran transfer. Ketiganya bekerja di lapisan yang berbeda — dan kalian akan memahami kapan harus memakai yang mana.

Redis Cache di Level Aplikasi

Client Redis

Redis sudah kita kenalkan di episode 20 (rate limit store). Sekarang ia menjadi cache data:

JSsrc/utils/cache.js - helper cache
import { Redis } from "ioredis"
import { config } from "../config.js"
 
export const cache = new Redis(config.redisUrl)
 
export async function getCached(key) {
  const value = await cache.get(key)
  return value ? JSON.parse(value) : null
}
 
export async function setCached(key, data, ttlSeconds) {
  await cache.set(key, JSON.stringify(data), "EX", ttlSeconds)
}
 
export async function invalidateKey(key) {
  await cache.del(key)
}
  • JSON.stringify/JSON.parse karena Redis menyimpan string — objek dikonversi eksplisit.
  • TTL ("EX") membuat cache kadaluwarsa otomatis — mencegah data basi menumpuk selamanya.

Caching Endpoint Read

Pola cache-aside: baca cache dulu; jika kosong, ambil dari database lalu simpan:

JSCache-aside untuk list users
import { getCached, setCached, invalidateKey } from "../utils/cache.js"
 
const LIST_CACHE_KEY = "users:list"
const TTL = 60
 
export const listUsers = async (req, res) => {
  const cached = await getCached(LIST_CACHE_KEY)
  if (cached) {
    return res.json({ data: cached, cached: true })
  }
 
  const users = await User.find({}).lean()
  await setCached(LIST_CACHE_KEY, users, TTL)
 
  res.json({ data: users, cached: false })
}

Beban database turun drastis: alih-alih query di setiap request, query berjalan paling banyak sekali per menit. Trade-offnya adalah stale data — cache 60 detik berarti data bisa tertinggal sampai 60 detik.

Invalidasi saat Mutasi

Cache hanya berguna jika di-invalidasi saat data berubah. Setiap operasi tulis menghapus cache terkait:

JSInvalidasi cache saat mutasi
export const createUser = async (req, res) => {
  const user = await User.create(req.body)
  await invalidateKey(LIST_CACHE_KEY)
  res.status(201).json({ data: user })
}
 
export const updateUser = async (req, res) => {
  const user = await User.findByIdAndUpdate(
    req.params.id,
    req.body,
    { new: true, runValidators: true }
  )
  await invalidateKey(LIST_CACHE_KEY)
  await invalidateKey(`users:${user.id}`)
  res.json({ data: user })
}

Important

Aturan emas cache-aside: setiap write meng-invalidate cache yang terkait. Melupakan invalidasi membuat aplikasi menyajikan data basi — bug yang licin karena tidak melempar error, hanya berperilaku aneh. Asumsikan selalu ada pola get-cache + invalidate dalam satu pasang.

HTTP Caching Headers dan ETag

ETag Bawaan Express

Express mengirim ETag (fingerprint konten) secara otomatis. Saat client mengirim If-None-Match, server mengembalikan 304 Not Modified tanpa body — hemat bandwidth:

JSETag bawaan Express
app.set("etag", "strong")

"strong" menghasilkan ETag per-byte (akurat untuk range request); "weak" per-konten. Default "weak" cukup untuk sebagian besar kasus.

Cache-Control untuk Aset

Untuk aset statis yang jarang berubah (episode 11), set header cache agresif:

JSCache-Control untuk aset statis
app.use(
  "/static",
  express.static("public", {
    setHeaders: (res) => {
      res.setHeader("Cache-Control", "public, max-age=86400")
    },
  })
)

max-age=86400 memberitahu browser menyimpan aset satu hari. Kombinasikan dengan penamaan file ber-hash (CSS/JS yang diberi hash pada nama) agar perubahan versi tidak tertelan cache lama.

Cache-Control untuk API

API JSON yang bergantung pada data pengguna biasanya memakai no-store — jangan disimpan:

JSAPI privat tidak di-cache
res.setHeader("Cache-Control", "no-store")

Respons privat (profil, data akun) tidak boleh di-cache bersama. Pilihan yang benar:

KontenCache-Control
Aset publik (CSS/JS/gambar)public, max-age=86400
Data publik yang jarang berubahpublic, max-age=300
Data per-user / privatno-store

Kompresi gzip

compression Middleware

Body JSON besar bisa dikompresi hingga ~80%:

Install compression
npm install compression
JSAktifkan kompresi
import compression from "compression"
 
app.use(compression())

Mengapa Jangan Kompres Semua

Respons yang sudah kecil (kurang dari ~1 KB) justru lebih lambat jika dikompresi (overhead CPU + header). compression punya threshold otomatis, dan aset gambar/video (sudah terkompresi) sebaiknya di-skip. Verifikasi kompresi bekerja:

Cek header Content-Encoding
curl -s -H "Accept-Encoding: gzip" \
  -D - http://localhost:3000/users -o /dev/null

Header Content-Encoding: gzip menandakan kompresi aktif. Sebagai catatan, di produksi gzip/br biasanya ditangani reverse proxy (episode 24) — middleware ini tetap berguna untuk kasus tanpa proxy.

Mengukur dan Menemukan Endpoint Lambat

Pengukuran Dasar

JSMiddleware pengukur latensi
export const latencyTimer = (req, res, next) => {
  const started = process.hrtime.bigint()
  res.on("finish", () => {
    const latencyMs = Number(process.hrtime.bigint() - started) / 1e6
    logger.info({ path: req.path, latencyMs, status: res.statusCode }, "request")
  })
  next()
}

Pola ini mengisi log structured episode 17 dengan data latensi per route — sumber kebenaran untuk menemukan endpoint yang perlu di-cache atau di-tuning.

Strategi Optimasi Berurutan

  1. Ukur (latency timer + logging).
  2. Identifikasi endpoint yang sering dipanggil dan lambat.
  3. Cache respons publik (cache-aside Redis).
  4. Index database untuk query yang lambat (episode 9-10).
  5. Kompresi jika transfer menjadi bottleneck.

Index MongoDB

Query find yang sering dan difilter butuh index:

JSIndex untuk query umum
userSchema.index({ email: 1 }, { unique: true })
userSchema.index({ createdAt: -1 })

Tanpa index, MongoDB melakukan collection scan — lambat pada data besar. Ini optimasi database yang melengkapi cache aplikasi.

Tip

Ukur sebelum dan sesudah setiap optimasi dengan autocannon atau k6 (load test formal kita bahas di episode 25). Optimasi tanpa pengukuran hanyalah tebakan — angka latensi dan throughput adalah hakim yang adil.

Common Pitfalls

Cache yang Tidak Pernah Di-invalidate

Data basi selamanya — pastikan setiap write meng-invalidate. Pola cache-aside yang disiplin menghindari ini.

TTL Terlalu Lama untuk Data Dinamis

Cache 1 jam untuk daftar stok yang berubah per detik adalah bencana. TTL mengikuti frekuensi perubahan data.

Meng-cache Respons Privat

Data per-user yang di-cache tanpa key per-user akan bocor ke user lain. Jika cache, sertakan identitas user di key (users:42:profile).

Memakai Redis untuk Segalanya

Redis cepat karena data di RAM — tetapi bukan pengganti database. Simpan yang sering dibaca dan jarang berubah; bukan seluruh state.

Note

Urutan pilihan cache di sistem produksi: CDN (paling luar) → HTTP cache browser (ETag/Cache-Control) → cache aplikasi Redis (paling dalam, paling dekat logika). Episode ini membangun lapisan dalam; lapisan luar kita sentuh lagi saat deployment di episode 24.

Penutup

Episode 21 mengoptimalkan performa: cache-aside Redis dengan invalidasi disiplin, HTTP caching headers dan ETag bawaan Express, kompresi gzip, serta pengukuran latensi yang memandu keputusan optimasi.

Inti yang harus dibawa pulang:

  • Cache-aside: baca cache → miss → query DB → simpan dengan TTL.
  • Setiap write meng-invalidate cache — aturan emas yang mencegah data basi.
  • ETag bawaan Express dan Cache-Control memangkas transfer; pilih header sesuai jenis konten.
  • compression() untuk gzip; lewati aset yang sudah terkompresi.
  • Ukur latensi per route (log structured) sebelum memutuskan optimasi.
  • Index database melengkapi cache aplikasi untuk query yang sering.

Di episode 22 selanjutnya kita akan membangun background jobs & queue — BullMQ dengan Redis, worker process, cron, dan pemindahan pekerjaan berat keluar dari request handler. Sampai jumpa di episode 22!

Belajar ExpressJS - Caching & Performance | Belajar ExpressJS