Skip to content

Performance

Optimization tips for GraphQL resolvers, caching strategies, and batching.

N+1 Query Problem

Problem

ts
// Bad: N+1 queries
export const postTypes = defineType({
  Post: {
    author: async (parent, _, context) => {
      // This runs once per post!
      return await context.db.user.findUnique({
        where: { id: parent.authorId }
      })
    },
  },
})

Solution: DataLoader

ts
import DataLoader from 'dataloader'

const userLoader = new DataLoader(async (ids: readonly string[]) => {
  const users = await db.user.findMany({
    where: { id: { in: [...ids] } }
  })
  return ids.map(id => users.find(u => u.id === id))
})

export const postTypes = defineType({
  Post: {
    author: parent => userLoader.load(parent.authorId),
  },
})

Caching

Field-Level Caching

ts
const cache = new Map()

export const postQueries = defineQuery({
  post: async (_, { id }) => {
    const cached = cache.get(id)
    if (cached)
      return cached

    const post = await db.post.findUnique({ where: { id } })
    cache.set(id, post)
    return post
  },
})

Redis Caching

ts
export const postQueries = defineQuery({
  posts: async (_, __, context) => {
    const cached = await context.redis.get('posts')
    if (cached)
      return JSON.parse(cached)

    const posts = await context.db.post.findMany()
    await context.redis.setEx('posts', 300, JSON.stringify(posts))
    return posts
  },
})

Query Optimization

Limit Fields

ts
export const userQueries = defineQuery({
  users: (_, { limit = 10 }) => {
    return db.user.findMany({
      take: limit,
      select: { id: true, name: true, email: true },
    })
  },
})

Pagination

ts
export const postQueries = defineQuery({
  posts: (_, { cursor, limit = 10 }) => {
    return db.post.findMany({
      take: limit,
      skip: cursor ? 1 : 0,
      cursor: cursor ? { id: cursor } : undefined,
    })
  },
})

Next Steps

Released under the MIT License.