tutorial

76 Optimasi Resolver agar Tidak Lambat

76 Optimasi Resolver agar Tidak Lambat: Panduan Lengkap untuk Engineer

Bayangkan sebuah sistem backend dengan GraphQL, puluhan query harian, dan traffic tinggi. Para engineer seringkali menjadikan resolver sebagai tempat utama proses fetching dan transformasi data. Namun, lambat laun, performa resolver menjadi masalah klasik—API terasa lambat, bahkan kadang membebani infrastruktur. Artikel ini membedah 76 optimasi yang bisa Anda lakukan agar resolver Anda tetap ringan, cepat, dan scalable.

Mengapa Resolver Sering Lambat?

Resolver ada di jantung ekosistem GraphQL. Ia memetakan query client ke resource backend (database, REST API, service lain). Permasalahan performa muncul karena beberapa faktor:

  • N+1 problems
  • Unoptimized queries
  • Overfetching/underfetching
  • Kurangnya caching
  • Pemrosesan data yang tidak efisien

Mari kita selami lapisan-lapisannya, dan saya akan tunjukkan 76 optimasi yang terbukti efektif.


1. Struktur Resolver yang Bersih

Struktur yang baik membuat debug, tracing, dan profiling lebih mudah.

// Resolver yang bagus: Modular, reusable, testable
module.exports = {
  Query: {
    user: async (parent, args, context) => {
      return context.dataSources.userAPI.getUserById(args.id);
    },
  },
  User: {
    posts: async (user, _, context) => {
      return context.dataSources.postAPI.getPostsByUser(user.id);
    },
  },
};

Hindari menulis semua logic langsung di resolver, apalagi query SQL/raw database di dalamnya.


2. Batasi Penggunaan Anonymous Function

Resolver yang inline dan tidak diberi nama rentan lupa ditest dan susah di-log. Biasakan pisahkan logic berat ke helper:

const getDetailedUser = (userId, dataSources) => { ... }

// Di resolver
user: (parent, args, context) => getDetailedUser(args.id, context.dataSources)

3-10. Atasi N+1 Query Problem

Skenario N+1 muncul saat fetching relasi, contohnya: Ambil user lalu ambil post dari tiap user.

Menggunakan DataLoader

Install dulu:

npm install dataloader

Implementasi:

const DataLoader = require('dataloader');

const postLoader = new DataLoader(async (userIds) => {
  // Query posts with IN(userIds)
  const posts = await db.posts.findAll({ where: { userId: userIds } });
  // Map result ke masing-masing owner id
  return userIds.map(id => posts.filter(p => p.userId == id));
});

Di resolver:

User: {
  posts: (user, args, { loaders }) => loaders.postLoader.load(user.id)
}

Performa membaik drastis!
Simulasi benchmark kecil:

Jumlah UserTanpa Dataloader (ms)Dengan Dataloader (ms)
1015240
100142368

11-15. Limit dan Pagination sebagai Aturan Wajib

Jangan pernah kirim seluruh data tanpa batas (limit), gunakan teknik offset, cursor, atau keyset pagination:

// Query dengan limit & offset
db.users.find({ limit: 50, offset: 0 });

16-20. Gunakan Projection/Field Mask

Cek hanya field yang diminta GraphQL:

function selectFields(info) {
  // Extract requested fields, implementasi custom
}

const fields = selectFields(info); // ['id','name']
const user = db.user.find({ select: fields });

21-30. Response Caching

Implement cache pada level dataSource atau resolver, contoh dengan Redis:

async function cachedFindUser(id) {
  const cachedUser = await redis.get("user:" + id);
  if (cachedUser) return JSON.parse(cachedUser);

  const user = await db.user.findById(id);
  await redis.set("user:" + id, JSON.stringify(user), 'EX', 3600);
  return user;
}

31-35. Query Batching

Gabungkan beberapa query menjadi satu, terutama saat request beruntun dari user yang sama.


36-45. Async/Await All the Way

Jangan pernah block event loop! Gunakan Promise.all jika resolver membutuhkan beberapa resource:

const [profile, posts] = await Promise.all([
  db.profile.findById(userId),
  db.posts.find({ userId })
]);

46-55. Monitoring dan Logging Detail

Pasang tracing:

const tracer = require('your-trace-lib');
tracer.startSpan('getUserResolver');
// resolve logic
tracer.endSpan();

56-60. Debounce dan Throttle Internal Request

Resolver yang memanggil resource eksternal (API third party) rawan membebani server mereka.


61-65. Early Return dan Filtering

Saring sebanyak mungkin di database, jangan di memory JS:

// Kurang optimal
db.users.find().filter(user => user.isActive);

// Lebih baik:
db.users.find({ isActive: true });

66-70. Batasi Nested Resolver (Depth Limit)

Agar resolver tidak infinite loop dan resource boros:

// graphql-depth-limit middleware usage
graphqlServer.use(depthLimit(5));

71-75. Dokumentasi dan Review Kode

Optimasi resolver yang baik selalu didukung test dan dokumentasi beri contoh edge-casenya.


76. Gunakan Profiling untuk Menemukan Bottleneck

Instal profiler (mis: 0x, clinic.js, perf_hooks):

npx 0x app.js

Analisa timeline functional resolver, temukan function yang bottleneck dan refactor.


Diagram Alur Resolver

Mari gambarkan alur eksekusi resolver teroptimasi dengan mermaid:

flowchart TD
    A[Query Masuk] --> B[Middleware Validasi & Auth]
    B --> C[Resolve Parent]
    C --> D((Cek Cache?))
    D -- Ya --> E[Return dari Cache]
    D -- Tidak --> F[Fecth DB/API/DataSource]
    F --> G[Process, Mapping, Transform]
    G --> H[Simpan ke Cache (optional)]
    H --> I[Kirim Response]

Kesimpulan

Mengoptimasi resolver itu bukan tugas sekali jadi. Setiap tahap perlu pendekatan berbeda—mulai dari arsitektur, resource management, caching, hingga profiling. Lakukan 76 tips di atas secara progresif, prioritaskan dari yang memiliki impact Google-style: 80% masalah resolver lambat biasanya hanya di <10 optimasi teratas!

Ingat, tujuan kita bukan sekedar “cepat”, tapi juga “robust” dan maintainable. Selalu review, monitor, dan refactor resolver secara berkala.


Bonus file:
Download template resolver siap optimized dan mulai benchmarking hari ini!


Related Reading:

  • How to Avoid GraphQL N+1 Problem with DataLoader (Medium)
  • Clean Resolver Patterns
  • Advanced GraphQL Caching Strategies

Selamat mengoptimasi! 🚀

comments powered by Disqus