Skip to content

Query Optimization

advanced
📜CoreGraph

GraphQL flexibility comes with a trade-off: poorly constructed queries can fetch too much data, skip caching layers, or trigger expensive server-side operations. Optimizely Graph provides several mechanisms to keep queries fast and efficient. Applying these patterns reduces latency, lowers infrastructure costs, and improves the end-user experience.

Graph uses a multi-layer caching strategy. Understanding each layer helps you design queries that benefit from caching rather than bypassing it.

Graph responses pass through a CDN layer before reaching clients. Identical queries receive cached responses without hitting the Graph backend. CDN caching works automatically for queries authenticated with a Single Key.

How CDN caching works:

  • The cache key includes the query string, variables, and authentication token
  • Cache duration depends on the configured TTL for your Graph instance
  • Cache invalidation happens automatically when content is synced

What bypasses CDN cache:

  • HMAC-authenticated queries (each signature is unique)
  • Queries with dynamic variables that change on every request
  • Mutations (Graph does not cache write operations)

Saved queries (also called persisted queries) are the most effective optimization technique. Instead of sending the full query text on every request, you register a query template with Graph and reference it by ID.

Using saved query templates
graphql
# Save this query as a template in the Graph admin
# Template name: GetLatestArticles
query GetLatestArticles($count: Int = 10, $locale: Locales = en) {
  ArticlePage(
    where: { _metadata: { status: { eq: "Published" } } }
    orderBy: { _metadata: { published: DESC } }
    limit: $count
    locale: $locale
  ) {
    items {
      Headline
      Author
      PublishDate
      _metadata {
        url { default }
      }
    }
    total
    cursor
  }
}
javascript
// Reference by template ID instead of sending full query
const response = await fetch(
  'https://cg.optimizely.com/content/v2',
  {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${SINGLE_KEY}`,
    },
    body: JSON.stringify({
      extensions: {
        persistedQuery: {
          version: 1,
          sha256Hash: 'abc123def456'
        }
      },
      variables: { count: 10, locale: 'en' }
    })
  }
);

Benefits of saved query templates:

  • Smaller request payloads — Send a hash instead of full query text
  • Better CDN caching — Cache keys are simpler and more stable
  • Security — Prevent arbitrary queries from untrusted clients
  • Validation — Templates are validated at registration time, not at runtime

See Use Cached Templates for setup steps.

When you know the specific content item you need, use item-level queries instead of list queries with filters. Item queries are faster because Graph can retrieve the item directly by its key.

Item query vs list query
graphql
query GetArticleByKey {
  ArticlePage(
    where: {
      _metadata: {
        key: { eq: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }
      }
    }
  ) {
    items {
      Headline
      Author
      ArticleBody { html }
    }
  }
}
graphql
query GetArticleByUrl {
  ArticlePage(
    where: {
      _metadata: {
        url: { default: { eq: "/articles/getting-started" } }
      }
    }
  ) {
    items {
      Headline
      Author
      ArticleBody { html }
    }
  }
}

GraphQL lets clients specify exactly which fields they need. Use this to your advantage — request only the fields your frontend actually renders.

# Avoid: fetching everything for a listing page
query { ArticlePage { items { Headline Author Body Tags Image RelatedArticles ... } } }
# Prefer: fetch only what the listing card displays
query { ArticlePage(limit: 10) { items { Headline Author PublishDate _metadata { url { default } } } } }

Smaller response payloads reduce network transfer time and improve CDN cache hit rates.

For paginated results, cursor-based pagination outperforms skip-based pagination, especially for deep pages. Skip-based pagination becomes slower as the offset increases because Graph must evaluate and discard all skipped items.

Pagination style1st page10th page100th page
CursorFastFastFast
SkipFastModerateSlow

Always prefer cursor pagination for user-facing features like infinite scroll or load-more patterns.

When using full-text search, boost fields that should weigh more heavily in relevance scoring. Headlines matching a search term are typically more relevant than body text matches.

Field-level boosting
graphql
query SearchArticles($term: String!) {
  ArticlePage(
    where: {
      _fulltext: {
        contains: $term
        boost: {
          Headline: 3
          Summary: 2
          ArticleBody: 1
        }
      }
    }
    orderBy: { _ranking: RELEVANCE }
    limit: 20
  ) {
    items {
      Headline
      Summary
      _score
    }
  }
}

Fetch facet counts in the same query as results to avoid separate roundtrips. Graph calculates facets server-side with near-zero overhead compared to a results-only query.

Avoid these common mistakes:

  • Unbounded queries — Always set a limit. The default of 20 may be fine for development, but explicit limits communicate intent and prevent accidental large fetches.
  • Over-fetching in loops — Do not query Graph inside a loop. Batch your data needs into a single query using filters or aliases.
  • Ignoring cursor — Discarding the cursor and using skip-based pagination adds unnecessary load for paginated UIs.
  • Fetching at render time — For static sites, query Graph at build time (SSG) rather than on every page view. Use webhooks to trigger rebuilds when content changes.