Cache dan revalidasi
Konfigurasi segmen route, unstable_cache, revalidateTag / revalidatePath, dan stale-while-revalidate.
Simpan respons route handler dan data dalam cache, lalu validasi ulang secara berkala atau sesuai permintaan. Entri kedaluwarsa langsung disajikan sementara satu pembaruan berjalan di latar belakang.
Route handler
Route handler GET bersifat dinamis secara default. Aktifkan cache melalui konfigurasi segmen route:
export const revalidate = 60;export async function GET() { const posts = await db.posts.latest(); return Response.json(posts);}| Ekspor | Perilaku |
|---|---|
| (tidak ada) | Berjalan pada setiap permintaan |
dynamic = "force-static" | Disimpan dalam cache hingga divalidasi ulang |
revalidate = N | Disimpan dalam cache; kedaluwarsa setelah N detik |
revalidate = 0 atau dynamic = "force-dynamic" | Berjalan pada setiap permintaan |
Nilai dynamic lainnya gagal dengan TX031. Ini termasuk "error" milik Next.js, karena Text.js tidak bisa mendeteksi kapan handler membaca permintaan.
Handler yang menggunakan cache mendapat satu entri per pathname, dan request-nya menyerupai kondisi saat build: tanpa string kueri, header, cookie, atau body. Baca semua itu dalam handler dinamis. Hanya GET dan HEAD yang disimpan dalam cache. Respons dengan status 5xx, header Set-Cookie, atau Cache-Control: private / no-store dikembalikan tetapi tidak pernah disimpan.
Header
Respons dari cache menyertakan:
| Header | Nilai |
|---|---|
x-textjs-cache | MISS, HIT, atau STALE |
Cache-Control | s-maxage=N, stale-while-revalidate=<1 year − N>, atau s-maxage=31536000 untuk force-static |
Ketika middleware menetapkan cookie, menulis ulang permintaan, atau menimpa header permintaan, header yang dihasilkan menjadi private, no-cache, no-store, max-age=0, must-revalidate, sehingga cache bersama tidak pernah menggunakan kembali keputusan khusus pengunjung. Header Cache-Control yang ditetapkan handler tetap dipertahankan.
Dalam text dev, konfigurasi segmen berlaku tetapi tidak ada yang disimpan, sehingga setiap permintaan menjalankan handler.
Cache data
Simpan fungsi asinkron apa pun dalam cache dengan unstable_cache dari @textjs/app/cache:
import { unstable_cache } from "@textjs/app/cache";export const getPosts = unstable_cache( async (author: string) => db.posts.byAuthor(author), ["posts"], { tags: ["posts"], revalidate: 300 },);Kunci terdiri dari bagian kunci ditambah argumen, yang dikodekan berdasarkan nilai (argumen undefined dan null, Map, Set, Date, dan URL tetap dibedakan). Tanpa bagian kunci, kode sumber fungsi dan tempat pembuatannya menjadi identitasnya; berikan bagian kunci setiap kali fungsi menangkap nilai dari lingkup luarnya, misalnya saat dibuat per permintaan atau dalam perulangan. Hasil diserialisasi ke JSON, sehingga setiap pemanggilan mengembalikan salinan baru (dan objek Date dikembalikan sebagai string); hasil yang tidak bisa diserialisasi memicu error. cache(fn, { key, tags, revalidate }) menerima pengaturan yang sama sebagai objek opsi.
Di dalam route handler yang menggunakan cache, entri data meneruskan tag dan revalidate terpendeknya ke route: memvalidasi ulang "posts" juga memperbarui /api/posts. Pemanggilan data dengan revalidate: 0 membuat route menjadi dinamis.
Stale-while-revalidate
Setelah usia entri melebihi revalidate:
- Permintaan berikutnya langsung menerima nilai kedaluwarsa (
STALE). - Satu pembaruan berjalan di latar belakang; permintaan bersamaan berbagi pembaruan tersebut.
- Jika pembaruan memicu error, nilai kedaluwarsa tetap disajikan dan error dicatat dalam log.
Di Vercel dan Cloudflare, pembaruan latar belakang didaftarkan dengan waitUntil(). Entri yang berusia lebih dari satu tahun selalu dihitung ulang di latar depan.
Validasi ulang sesuai permintaan
import { revalidatePath, revalidateTag } from "@textjs/app/cache";export async function POST(request: Request) { // Anyone can POST here: require a secret shared with your CMS or webhook. if (request.headers.get("authorization") !== `Bearer ${process.env.REVALIDATE_SECRET}`) { return new Response("Unauthorized", { status: 401 }); } await revalidateTag("posts"); await revalidatePath("/api/feed"); return Response.json({ revalidated: true });}| Pemanggilan | Yang dibatalkan validitasnya |
|---|---|
revalidateTag("posts") | Semua yang diberi tag posts; permintaan berikutnya menghitung ulang |
revalidateTag("posts", "max") | Menandainya kedaluwarsa; permintaan berikutnya menerima nilai kedaluwarsa selama pembaruan berjalan |
revalidatePath("/api/feed") | Path tersebut |
revalidatePath("/api/posts/[id]") | Setiap path dari route dinamis |
revalidatePath("/api", "layout") | Semua yang berada di bawah /api |
Keduanya mengembalikan promise. Dengan penyimpanan dalam memori, pembatalan validitas berlangsung seketika; gunakan await jika penyimpanan kustom bersifat asinkron.
Penyimpanan cache kustom
Penyimpanan default adalah LRU dalam memori (1.000 entri, sekitar 50 MB) per instans server. Untuk berbagi cache antarinstans, arahkan cacheHandler ke modul yang mengekspor CacheStore sebagai ekspor default:
export default defineConfig({ cacheHandler: "./cache-handler.ts" });import type { CacheStore } from "@textjs/app/cache";const store: CacheStore = { async get(key) { return redis.get(key).then((v) => (v ? JSON.parse(v) : undefined)); }, async set(key, entry) { await redis.set(key, JSON.stringify(entry)); }, async delete(key) { await redis.del(key); }, async markTags(tags, mode, at) { for (const t of tags) await redis.hset(`tag:${t}`, mode, at); }, async getTagState(tags) { /* max expiredAt / staleAt across tags */ return { expiredAt: 0, staleAt: 0 }; },};export default store;Entri berupa JSON biasa. Sebagai alternatif, setCacheStore(store) memasang penyimpanan saat runtime.
Catatan platform
- Setiap instans serverless (fungsi Vercel, isolate Cloudflare) memiliki cache dalam memorinya sendiri.
Cache-Control: s-maxagememungkinkan CDN platform menyimpan respons dalam cache juga; validasi ulang sesuai permintaan tidak menghapus cache CDN. GunakancacheHandlerbersama ketikarevalidateTagharus menjangkau setiap instans. - Cloudflare memerlukan flag
nodejs_compat(cache menggunakanAsyncLocalStorage). Lihat Deploy.