Episode 9 menaklukkan N+1 problem, masalah performa klasik GraphQL: memahami penyebab N+1 saat resolver chain memicu query berulang, konsep batching dan caching DataLoader, implementasi batch loader, integrasi loader ke context per request, hingga pengukuran perbaikan performa.

Episode 6 sempat menyinggung bahaya resolver chain. Kini saatnya membedah bahaya tersebut secara mendalam: N+1 problem, musuh performa paling terkenal di GraphQL. Episode 9 menjelaskan mengapa GraphQL rentan terhadapnya dan bagaimana DataLoader menjadi solusi standarnya.
Kita akan memahami mekanisme N+1, konsep batching dan caching DataLoader, cara mengimplementasikannya, integrasi loader ke dalam context yang per-request, hingga teknik mengukur perbaikan performa setelah loader dipasang.
Perhatikan query berikut:
query DaftarPost {
posts {
id
title
author {
name
}
}
}Jika ada 100 post, resolver posts memicu 1 query, lalu resolver Post.author dipanggil 100 kali — satu kali per post — dan masing-masing memicu 1 query ke tabel users. Total 1 + 100 = 101 query untuk satu request. Itulah asal nama N+1.
Penyebabnya: GraphQL memanggil resolver per-field-per-item, dan default resolver mengembalikan data dari parent secara independen. Pola ini membuat query terlihat efisien di client, tapi boros di database.
Dampaknya nyata: latensi request membengkak, database kehabisan koneksi, dan biaya infrastruktur naik. Di production dengan ribuan user, N+1 adalah masalah nomor satu yang ditemukan saat load testing. Identifikasi awal bisa dilakukan dengan melihat log query: jika muncul pola query identik berulang kali dengan id berbeda, hampir pasti N+1.
DataLoader adalah library yang dirancang untuk masalah ini dengan dua mekanisme:
load dalam satu tick event loop digabung menjadi satu pemanggilan batch function.npm install dataloaderJalankan npm install dataloader untuk menambahkan library ini ke project.
import DataLoader from "dataloader";
const batchUsers = async (ids) => {
const users = await db.users.findMany({ where: { id: { in: ids } } });
const byId = new Map(users.map((u) => [u.id, u]));
return ids.map((id) => byId.get(id));
};
const userLoader = new DataLoader(batchUsers);Perhatikan dua detail penting. Pertama, fungsi batch menerima array ids dan harus mengembalikan hasil dengan urutan yang sama seperti input. Kedua, jika sebuah id tidak ditemukan, kembalikan null untuk posisinya agar urutan tidak bergeser.
Loader mengubah resolver Post.author dari query per-item menjadi satu query batch:
const resolvers = {
Query: {
posts: async (_, __, ctx) => ctx.db.posts.findMany(),
},
Post: {
author: (post, _, ctx) => ctx.loaders.userById.load(post.authorId),
},
};Sekarang 100 post hanya memicu 1 query users. Batching bekerja karena semua panggilan load dari resolver-resolver yang berjalan paralel terkumpul dalam satu tick, lalu digabung oleh DataLoader.
Cache DataLoader bersifat per-instance. Karena itu loader harus dibuat per request, bukan global — jika global, data yang sudah berubah (misalnya setelah mutation) akan tertutup cache lama. Untuk data yang baru saja dimutasi, panggil loader.clear(key) agar di-fetch ulang, atau bangun loader baru di setiap request.
Jika batch function melempar error, seluruh load dalam batch tersebut gagal. Untuk penanganan per-item yang lebih halus, kembalikan hasil per item dan throw error secara selektif. Pola umum: kembalikan null untuk item yang tidak ditemukan, dan baru throw bila seluruh batch bermasalah.
Tempat yang tepat untuk loader adalah di dalam fungsi context yang dipanggil per request:
const { url } = await startStandaloneServer(server, {
context: async ({ req }) => ({
user: await authenticateUser(req.headers.authorization),
db,
loaders: {
userById: new DataLoader(batchUsers),
postByAuthorId: new DataLoader(batchPostsByAuthor),
commentByPostId: new DataLoader(batchCommentsByPost),
},
}),
});Dengan pola ini, setiap request mendapat instance loader baru: batching berlaku per request, dan cache tidak bocor antar request. Semua resolver memakai loader yang sama melalui ctx.loaders, sehingga tidak ada duplikasi definisi.
Ukur jumlah query database sebelum dan sesudah memakai DataLoader. Alat sederhananya: hitung query di log, atau gunakan plugin Prisma untuk logging. Setelah loader terpasang, pola query identik berulang harus menghilang dan digantikan satu query batch dengan IN.
Untuk debugging batch call, tambahkan log di dalam batch function — kalian akan melihat ids terkumpul dalam satu array, bukan dipanggil satu-satu. Ini bukti batching bekerja. DataLoader juga bisa diberi opsi maxBatchSize bila satu request bisa menghasilkan batch raksasa.
Inti yang harus dibawa pulang:
load menjadi satu batch function call.loader.clear(key) setelah mutation agar data segar.Di episode 10 selanjutnya kalian akan mempelajari validasi dan sanitasi input — validasi otomatis di level schema, integrasi Zod untuk validasi resolvers, sanitasi untuk mencegah XSS dan SQL injection, hingga desain error response yang ramah client. Data yang masuk ke server kalian tidak akan lagi lepas kontrol!