shopify-performance-tuning

Installation
SKILL.md

Shopify Performance Tuning

Overview

Optimize Shopify API performance through GraphQL query cost reduction, bulk operations for large data exports, response caching, and Storefront API for high-traffic public-facing queries.

Prerequisites

  • Understanding of Shopify's calculated query cost system
  • Access to the Shopify-GraphQL-Cost-Debug: 1 header for cost analysis
  • Redis or in-memory cache available (optional)

Instructions

Step 1: Analyze Query Cost

# Debug query cost with special header
curl -X POST "https://$STORE/admin/api/2024-10/graphql.json" \
  -H "X-Shopify-Access-Token: $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Shopify-GraphQL-Cost-Debug: 1" \
  -d '{"query": "{ products(first: 50) { edges { node { id title variants(first: 20) { edges { node { id price } } } } } } }"}' \
  | jq '.extensions.cost'

Response shows cost breakdown:

{
  "requestedQueryCost": 152,
  "actualQueryCost": 42,
  "throttleStatus": {
    "maximumAvailable": 1000.0,
    "currentlyAvailable": 958.0,
    "restoreRate": 50.0
  }
}

Key rule: requestedQueryCost is calculated as first * nested_fields. Reducing first: from 250 to 50 can cut cost by 5x.

Step 2: Reduce Query Cost

// BEFORE: High cost — requests too many fields and items
// requestedQueryCost: ~5,502
const EXPENSIVE_QUERY = `{
  products(first: 250) {
    edges {
      node {
        id title description descriptionHtml vendor productType tags
        variants(first: 100) {
          edges {
            node {
              id title price compareAtPrice sku barcode
              inventoryQuantity weight weightUnit
              selectedOptions { name value }
              metafields(first: 10) {
                edges { node { namespace key value type } }
              }
            }
          }
        }
        images(first: 20) {
          edges { node { url altText width height } }
        }
        metafields(first: 10) {
          edges { node { namespace key value type } }
        }
      }
    }
  }
}`;

// AFTER: Optimized — only needed fields, smaller page sizes
// requestedQueryCost: ~112
const OPTIMIZED_QUERY = `{
  products(first: 50) {
    edges {
      node {
        id
        title
        status
        variants(first: 5) {
          edges {
            node { id price sku inventoryQuantity }
          }
        }
      }
    }
    pageInfo { hasNextPage endCursor }
  }
}`;

Step 3: Use Bulk Operations for Large Exports

Bulk operations bypass rate limits and are designed for exporting large datasets:

// Step 1: Start bulk operation
const START_BULK = `
  mutation {
    bulkOperationRunQuery(query: """
      {
        products {
          edges {
            node {
              id
              title
              handle
              variants {
                edges {
                  node {
                    id
                    sku
                    price
                    inventoryQuantity
                  }
                }
              }
            }
          }
        }
      }
    """) {
      bulkOperation {
        id
        status
      }
      userErrors { field message }
    }
  }
`;

// Step 2: Poll for completion
const CHECK_BULK = `{
  currentBulkOperation {
    id
    status       # CREATED, RUNNING, COMPLETED, FAILED
    errorCode
    objectCount
    fileSize
    url          # JSONL download URL when COMPLETED
    createdAt
  }
}`;

// Step 3: Download results (JSONL format — one JSON object per line)
// const response = await fetch(bulkOperation.url);
// Each line: {"id":"gid://shopify/Product/123","title":"Widget",...}

Step 4: Cache Frequently Accessed Data

import { LRUCache } from "lru-cache";

const shopifyCache = new LRUCache<string, any>({
  max: 500,
  ttl: 5 * 60 * 1000, // 5 minutes
  updateAgeOnGet: true,
});

async function cachedQuery<T>(
  cacheKey: string,
  queryFn: () => Promise<T>,
  ttlMs?: number
): Promise<T> {
  const cached = shopifyCache.get(cacheKey);
  if (cached !== undefined) return cached as T;

  const result = await queryFn();
  shopifyCache.set(cacheKey, result, { ttl: ttlMs });
  return result;
}

// Usage — cache product data for 5 minutes
const product = await cachedQuery(
  `product:${productId}`,
  () => shopifyQuery(shop, PRODUCT_QUERY, { id: productId })
);

// Invalidate on webhook
app.post("/webhooks", (req, res) => {
  const topic = req.headers["x-shopify-topic"];
  if (topic === "products/update") {
    const payload = JSON.parse(req.body);
    shopifyCache.delete(`product:gid://shopify/Product/${payload.id}`);
  }
});

Step 5: Use Storefront API for Public Queries

The Storefront API has separate rate limits and is designed for high-traffic public storefronts:

// Storefront API — safe for client-side, higher rate limits
const storefrontClient = new shopify.clients.Storefront({
  session,
  apiVersion: "2024-10",
});

// Storefront API query — no admin credentials exposed
const products = await storefrontClient.request(`{
  products(first: 12, sortKey: BEST_SELLING) {
    edges {
      node {
        id
        title
        handle
        priceRange {
          minVariantPrice { amount currencyCode }
        }
        featuredImage {
          url(transform: { maxWidth: 400 })
          altText
        }
      }
    }
  }
}`);

Output

  • Query costs reduced through field selection and page size optimization
  • Bulk operations configured for large data exports
  • Response caching with webhook-driven invalidation
  • Storefront API used for public-facing high-traffic queries

Error Handling

Issue Cause Solution
THROTTLED on every query requestedQueryCost too high Reduce first: and remove unused fields
Bulk operation FAILED Query syntax error Test query in GraphiQL first
Stale cache data Cache not invalidated Add webhook handlers to clear cache
Storefront API 403 Wrong token type Use Storefront API access token, not Admin

Examples

Performance Comparison

Approach 10K Products Export Rate Limit Impact
Paginated (first: 250) 40 queries, ~60s Uses ~6,000 points
Paginated (first: 50) 200 queries, ~300s Uses ~22,000 points
Bulk Operation 1 query + poll, ~30s Minimal impact

Resources

Next Steps

For cost optimization, see shopify-cost-tuning.

Weekly Installs
1
GitHub Stars
2.1K
First Seen
Mar 26, 2026