Skip to content

Performance Tips ​

This guide covers best practices for optimizing GraphQL query performance when using GQLB. Learn how to write efficient queries, reduce payload sizes, and improve application performance.

Query Optimization ​

Field Selection Best Practices ​

Only select the fields you actually need:

typescript
// ❌ Bad: Selecting unnecessary fields
const INEFFICIENT_QUERY = b.query('GetPosts', (b) => [
  b.posts(b => [
    b.id(),
    b.title(),
    b.content(),        // Large field, not always needed
    b.description(),
    b.metadata(),       // Complex object, rarely used
    b.author(b => [
      b.id(),
      b.name(),
      b.email(),        // Sensitive data, not needed for display
      b.profile(b => [
        b.bio(),
        b.socialLinks(), // Complex nested data
        b.preferences(), // User-specific data
      ])
    ])
  ])
])

// ✅ Good: Minimal field selection
const EFFICIENT_QUERY = b.query('GetPosts', (b) => [
  b.posts(b => [
    b.id(),
    b.title(),
    b.description(),
    b.author(b => [
      b.id(),
      b.name(),
      b.avatar(),      // Only what's needed for UI
    ])
  ])
])

Pagination Instead of Large Lists ​

Use pagination to limit data transfer:

typescript
// ❌ Bad: Loading all posts at once
const ALL_POSTS_QUERY = b.query('GetAllPosts', (b) => [
  b.posts(b => [
    b.id(),
    b.title(),
    b.content(),
  ])
])

// ✅ Good: Paginated posts
const PAGINATED_POSTS_QUERY = b.query('GetPaginatedPosts', {
  first: 'Int!',
  after: 'String'
}, (b, v) => [
  b.posts({
    first: v.first,
    after: v.after
  }, b => [
    b.pageInfo(b => [
      b.hasNextPage(),
      b.endCursor(),
    ]),
    b.edges(b => [
      b.cursor(),
      b.node(b => [
        b.id(),
        b.title(),
        b.excerpt(),  // Use excerpt instead of full content
      ])
    ])
  ])
])

Fragment Optimization ​

Reusable Fragments ​

Create fragments for commonly used field sets:

typescript
// Define reusable fragments
const USER_CARD_FRAGMENT = b.fragment('UserCard', 'User', (b) => [
  b.id(),
  b.name(),
  b.avatar(),
])

const POST_PREVIEW_FRAGMENT = b.fragment('PostPreview', 'Post', (b) => [
  b.id(),
  b.title(),
  b.excerpt(),
  b.publishedAt(),
  b.readTime(),
])

// Use fragments in multiple queries
const DASHBOARD_QUERY = b.query('GetDashboard', (b) => [
  b.currentUser(b => [
    ...USER_CARD_FRAGMENT,
    b.followersCount(),
  ]),
  
  b.recentPosts({ first: 5 }, b => [
    ...POST_PREVIEW_FRAGMENT,
    b.author(b => [
      ...USER_CARD_FRAGMENT,
    ])
  ]),
  
  b.suggestedUsers({ first: 3 }, b => [
    ...USER_CARD_FRAGMENT,
    b.mutualFollowersCount(),
  ])
])

Fragment Composition ​

Compose larger fragments from smaller ones:

typescript
const USER_BASIC_FRAGMENT = b.fragment('UserBasic', 'User', (b) => [
  b.id(),
  b.name(),
  b.avatar(),
])

const USER_EXTENDED_FRAGMENT = b.fragment('UserExtended', 'User', (b) => [
  ...USER_BASIC_FRAGMENT,
  b.bio(),
  b.website(),
  b.joinedAt(),
])

const USER_FULL_FRAGMENT = b.fragment('UserFull', 'User', (b) => [
  ...USER_EXTENDED_FRAGMENT,
  b.stats(b => [
    b.postsCount(),
    b.followersCount(),
    b.followingCount(),
  ]),
  b.settings(b => [
    b.theme(),
    b.notifications(),
  ])
])

Conditional Field Loading ​

Variable-Driven Conditionals ​

Use variables to conditionally load expensive fields:

typescript
const CONDITIONAL_USER_QUERY = b.query('GetUser', {
  userId: 'ID!',
  includeStats: 'Boolean!',
  includePosts: 'Boolean!',
  includeSettings: 'Boolean!'
}, (b, v) => [
  b.user({ id: v.userId }, b => [
    // Always load basic info
    b.id(),
    b.name(),
    b.avatar(),
    
    // Conditionally load expensive data
    ...(b.if(v.includeStats, [
      b.stats(b => [
        b.postsCount(),
        b.followersCount(),
        b.totalLikes(),
      ])
    ])),
    
    ...(b.if(v.includePosts, [
      b.recentPosts({ first: 5 }, b => [
        b.id(),
        b.title(),
        b.publishedAt(),
      ])
    ])),
    
    ...(b.if(v.includeSettings, [
      b.settings(b => [
        b.theme(),
        b.notifications(),
        b.privacy(),
      ])
    ]))
  ])
])

// Usage: Load only what's needed
const basicUser = await executeQuery(CONDITIONAL_USER_QUERY, {
  userId: "123",
  includeStats: false,
  includePosts: false,
  includeSettings: false
})

const fullUser = await executeQuery(CONDITIONAL_USER_QUERY, {
  userId: "123",
  includeStats: true,
  includePosts: true,
  includeSettings: true
})

Batching and Deduplication ​

DataLoader Pattern ​

Structure queries to leverage DataLoader batching:

typescript
// ❌ Bad: N+1 queries in the UI
const USER_POSTS_QUERY = b.query('GetUserPosts', { userId: 'ID!' }, (b, v) => [
  b.user({ id: v.userId }, b => [
    b.id(),
    b.name(),
    b.posts(b => [
      b.id(),
      b.title(),
      b.comments(b => [  // This could cause N+1 for each post
        b.id(),
        b.author(b => [ // And N+1 for each comment author
          b.id(),
          b.name(),
        ])
      ])
    ])
  ])
])

// ✅ Good: Structured to leverage batching
const OPTIMIZED_USER_POSTS = b.query('GetOptimizedUserPosts', { 
  userId: 'ID!',
  postsFirst: 'Int!',
  commentsFirst: 'Int!'
}, (b, v) => [
  b.user({ id: v.userId }, b => [
    b.id(),
    b.name(),
    b.posts({ first: v.postsFirst }, b => [
      b.id(),
      b.title(),
      b.commentsCount(),  // Just the count, not full comments
      
      // Load comments separately when needed
      b.recentComments({ first: v.commentsFirst }, b => [
        b.id(),
        b.content(),
        b.authorId(),     // Just ID for batching
      ])
    ])
  ])
])

Batch Multiple Operations ​

Combine related queries into a single request:

typescript
const DASHBOARD_BATCH_QUERY = b.query('GetDashboardData', {
  userId: 'ID!'
}, (b, v) => [
  // User data
  b.user({ id: v.userId }, b => [
    b.id(),
    b.name(),
    b.avatar(),
  ]),
  
  // User's posts
  b.userPosts({ userId: v.userId, first: 10 }, b => [
    b.id(),
    b.title(),
    b.likesCount(),
  ]),
  
  // User's notifications
  b.userNotifications({ userId: v.userId, first: 5 }, b => [
    b.id(),
    b.type(),
    b.message(),
    b.read(),
  ]),
  
  // Site statistics
  b.siteStats(b => [
    b.totalUsers(),
    b.totalPosts(),
    b.activeUsers(),
  ])
])

Cache Optimization ​

Normalized Cache Keys ​

Structure queries to leverage normalized caching:

typescript
// ✅ Good: Consistent field selection for caching
const USER_CACHE_FRAGMENT = b.fragment('UserCache', 'User', (b) => [
  b.id(),           // Essential for cache normalization
  b.name(),
  b.avatar(),
  b.updatedAt(),    // For cache invalidation
])

const POST_CACHE_FRAGMENT = b.fragment('PostCache', 'Post', (b) => [
  b.id(),
  b.title(),
  b.publishedAt(),
  b.updatedAt(),
])

// Use consistent fragments across queries
const FEED_QUERY = b.query('GetFeed', (b) => [
  b.posts(b => [
    ...POST_CACHE_FRAGMENT,
    b.author(b => [
      ...USER_CACHE_FRAGMENT,
    ])
  ])
])

const USER_PROFILE_QUERY = b.query('GetUserProfile', { id: 'ID!' }, (b, v) => [
  b.user({ id: v.id }, b => [
    ...USER_CACHE_FRAGMENT,  // Same fragment = cache hit
    b.bio(),
    b.posts(b => [
      ...POST_CACHE_FRAGMENT,  // Same fragment = cache hit
    ])
  ])
])

Cache-First Queries ​

Design queries that work well with cache-first policies:

typescript
const CACHE_OPTIMIZED_QUERY = b.query('GetCacheOptimized', {
  userId: 'ID!'
}, (b, v) => [
  b.user({ id: v.userId }, b => [
    // Stable fields (good for caching)
    b.id(),
    b.name(),
    b.avatar(),
    b.bio(),
    
    // Separate frequently changing data
    b.stats(b => [
      b.postsCount(),      // Changes when posts are added
      b.followersCount(),  // Changes when followed/unfollowed
    ]),
    
    // Paginated data (cache by cursor)
    b.posts({ first: 10 }, b => [
      b.pageInfo(b => [
        b.endCursor(),
        b.hasNextPage(),
      ]),
      b.edges(b => [
        b.cursor(),
        b.node(b => [
          b.id(),
          b.title(),
          b.publishedAt(),  // Stable once published
        ])
      ])
    ])
  ])
])

Network Optimization ​

Reduce Query Depth ​

Limit nesting depth to reduce complexity:

typescript
// ❌ Bad: Deep nesting
const DEEP_QUERY = b.query('DeepQuery', (b) => [
  b.user(b => [
    b.posts(b => [
      b.comments(b => [
        b.replies(b => [
          b.author(b => [
            b.followers(b => [  // 6 levels deep!
              b.posts(b => [
                b.title()
              ])
            ])
          ])
        ])
      ])
    ])
  ])
])

// ✅ Good: Shallow queries with targeted requests
const SHALLOW_USER_QUERY = b.query('GetUser', (b) => [
  b.user(b => [
    b.id(),
    b.name(),
    b.postsCount(),  // Just counts, not full data
  ])
])

const USER_POSTS_QUERY = b.query('GetUserPosts', { userId: 'ID!' }, (b, v) => [
  b.posts({ authorId: v.userId }, b => [
    b.id(),
    b.title(),
    b.commentsCount(),  // Just counts
  ])
])

Optimize Variable Usage ​

Use variables efficiently to enable query caching:

typescript
// ❌ Bad: Inline values prevent caching
const INLINE_QUERY = b.query('GetPostsInline', (b) => [
  b.posts({ 
    first: 10,           // Hardcoded values
    status: "PUBLISHED", // can't be cached for different values
    orderBy: "DATE_DESC"
  }, b => [
    b.id(),
    b.title(),
  ])
])

// ✅ Good: Parameterized query enables caching
const PARAMETERIZED_QUERY = b.query('GetPosts', {
  first: 'Int!',
  status: 'PostStatus!',
  orderBy: 'PostOrderBy!'
}, (b, v) => [
  b.posts({ 
    first: v.first,
    status: v.status,
    orderBy: v.orderBy
  }, b => [
    b.id(),
    b.title(),
  ])
])

Error Performance ​

Early Error Detection ​

Structure queries to fail fast:

typescript
const ROBUST_QUERY = b.query('GetRobustData', {
  userId: 'ID!'
}, (b, v) => [
  // Check user exists first (fast operation)
  b.userExists({ id: v.userId }),
  
  // Only proceed with expensive operations if user exists
  b.user({ id: v.userId }, b => [
    b.id(),
    b.name(),
    
    // Expensive nested data only if needed
    b.analytics(b => [
      b.pageViews(),
      b.engagement(),
      b.demographics(),
    ])
  ])
])

Graceful Degradation ​

Design queries that can partially succeed:

typescript
const GRACEFUL_QUERY = b.query('GetDashboard', (b) => [
  // Essential data (must succeed)
  b.currentUser(b => [
    b.id(),
    b.name(),
  ]),
  
  // Optional data (can fail gracefully)
  b.recommendations(b => [
    b.id(),
    b.title(),
    b.score(),
  ]),
  
  // Analytics data (nice to have)
  b.analytics(b => [
    b.summary(b => [
      b.views(),
      b.engagement(),
    ])
  ])
])

// Handle partial failures
function handleDashboardData(data: DashboardData) {
  // Always have user data
  displayUser(data.currentUser)
  
  // Recommendations might be null if service is down
  if (data.recommendations) {
    displayRecommendations(data.recommendations)
  } else {
    showRecommendationsPlaceholder()
  }
  
  // Analytics might be null if not available
  if (data.analytics) {
    displayAnalytics(data.analytics)
  }
}

Monitoring and Profiling ​

Query Complexity Analysis ​

Add complexity analysis to queries:

typescript
const ANALYZED_QUERY = b.query('GetAnalyzedData', {
  depth: 'Int!',
  breadth: 'Int!'
}, (b, v) => [
  // Document complexity in comments
  b.posts({ 
    first: v.breadth  // Complexity: O(breadth)
  }, b => [
    b.id(),
    b.title(),
    
    b.comments({ 
      first: v.depth  // Complexity: O(breadth * depth)
    }, b => [
      b.id(),
      b.content(),
      
      b.author(b => [  // Complexity: O(breadth * depth * 1)
        b.id(),
        b.name(),
      ])
    ])
  ])
])

// Calculate complexity
const complexity = breadth * depth * 1  // = breadth * depth

Performance Tracking ​

Add query timing and metrics:

typescript
class QueryPerformanceTracker {
  async executeWithTracking<T>(
    query: Operation<T>,
    variables: VariablesOf<typeof query>
  ) {
    const startTime = performance.now()
    const operationName = query.operationName
    
    try {
      const result = await executeQuery(query, variables)
      const duration = performance.now() - startTime
      
      // Log successful queries
      console.log(`Query ${operationName} completed in ${duration}ms`)
      
      // Track metrics
      this.trackQueryMetrics(operationName, duration, 'success')
      
      return result
    } catch (error) {
      const duration = performance.now() - startTime
      
      // Log failed queries
      console.error(`Query ${operationName} failed after ${duration}ms`, error)
      
      // Track error metrics
      this.trackQueryMetrics(operationName, duration, 'error')
      
      throw error
    }
  }
  
  private trackQueryMetrics(name: string, duration: number, status: string) {
    // Send to analytics service
    analytics.track('graphql_query', {
      operation_name: name,
      duration_ms: duration,
      status: status,
      timestamp: Date.now()
    })
  }
}

Best Practices Summary ​

Query Design Checklist ​

  • ✅ Select only needed fields
  • ✅ Use pagination for lists
  • ✅ Leverage fragments for reuse
  • ✅ Use variables for parameterization
  • ✅ Limit query depth
  • ✅ Design for cache efficiency
  • ✅ Handle errors gracefully
  • ✅ Monitor query performance

Performance Monitoring ​

typescript
const PERFORMANCE_QUERY = b.query('GetPerformanceData', {
  includeExpensive: 'Boolean!'
}, (b, v) => [
  // Fast, always-included data
  b.user(b => [
    b.id(),
    b.name(),
  ]),
  
  // Expensive, conditionally-included data
  ...(b.if(v.includeExpensive, [
    b.expensiveAnalytics(b => [
      b.complexCalculation(),
      b.heavyAggregation(),
    ])
  ]))
])

// Use performance budgets
const PERFORMANCE_BUDGET = {
  maxQueryTime: 500,     // 500ms max
  maxFields: 50,         // 50 fields max
  maxDepth: 5,           // 5 levels max
  maxComplexity: 1000    // Complexity score max
}

Next Steps ​

Released under the MIT License.