Query Optimization
Why optimization matters
Section titled “Why optimization matters”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.
Caching layers
Section titled “Caching layers”Graph uses a multi-layer caching strategy. Understanding each layer helps you design queries that benefit from caching rather than bypassing it.
CDN caching
Section titled “CDN caching”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 query templates
Section titled “Saved query templates”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.
# 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
}
} // 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.
Query design patterns
Section titled “Query design patterns”Item queries for single content
Section titled “Item queries for single content”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.
query GetArticleByKey {
ArticlePage(
where: {
_metadata: {
key: { eq: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }
}
}
) {
items {
Headline
Author
ArticleBody { html }
}
}
} query GetArticleByUrl {
ArticlePage(
where: {
_metadata: {
url: { default: { eq: "/articles/getting-started" } }
}
}
) {
items {
Headline
Author
ArticleBody { html }
}
}
} Select only needed fields
Section titled “Select only needed fields”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 pagequery { ArticlePage { items { Headline Author Body Tags Image RelatedArticles ... } } }
# Prefer: fetch only what the listing card displaysquery { ArticlePage(limit: 10) { items { Headline Author PublishDate _metadata { url { default } } } } }Smaller response payloads reduce network transfer time and improve CDN cache hit rates.
Cursor pagination over skip
Section titled “Cursor pagination over skip”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 style | 1st page | 10th page | 100th page |
|---|---|---|---|
| Cursor | Fast | Fast | Fast |
| Skip | Fast | Moderate | Slow |
Always prefer cursor pagination for user-facing features like infinite scroll or load-more patterns.
Search optimization
Section titled “Search optimization”Boosting relevance
Section titled “Boosting relevance”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.
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
}
}
} Combine facets with filters
Section titled “Combine facets with filters”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.
Performance anti-patterns
Section titled “Performance anti-patterns”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.