Optimizing Frontend Payloads with GROQ Projections
Stop over-fetching CMS data. Learn how to use GROQ projections and dereferencing in Sanity to send only the necessary fields to your frontend, reducing latency and payload size.
10 Sept 2025, 09:36 UTC

The Over-Fetching Problem in Headless CMS
When using a headless CMS, it is common to fetch a whole document and filter the data on the frontend. However, as your content model grows—adding SEO metadata, internal audit fields, and complex references—fetching the entire JSON object increases latency and consumes unnecessary bandwidth. This is particularly problematic for mobile users or pages with dozens of content components.
The solution is to move the data shaping logic from the client to the server. In Sanity, this is achieved through GROQ (Graph-Relational Query Language) projections. Instead of receiving everything the CMS stores, you define a precise map of exactly what the frontend component needs to render.
How Projections Shape Your Data
A projection in GROQ is defined by curly braces {} following a filter. It tells the Content Lake: "I found these documents, but only send me these specific fields."
This approach prevents "leaking" internal CMS data (like _rev or _updatedAt) to the public API and ensures the frontend receives a clean, predictable object. When combined with dereferencing—the process of following a reference link to another document using the -> operator—you can flatten complex relational data into a single request.
Worked Example: Flattening a Product Page
Imagine a content model where a product document references an author document. Without projections, you would get a reference ID (e.g., "drafts.123") and would have to make a second API call to get the author's name.
Run the following query in the Sanity Vision plugin (the built-in query explorer in the Studio) to see how to project and dereference in one go:
// Fetch a single product by slug
*[_type == "product" && slug.current == "leather-boots"][0] {
title,
price,
"authorName": author->name,
"authorBio": author->bio,
"mainImage": mainImage.asset->url
}
Breakdown of this operation:
[0]: Ensures we get a single object instead of an array."authorName": author->name: This creates a virtual field. It follows the reference in theauthorfield and extracts only thenamestring.mainImage.asset->url: Navigates the image asset reference to retrieve the actual CDN URL.
Performance Trade-offs and Limitations
While projections are powerful, deeply nested dereferencing can impact query performance. If you are dereferencing multiple levels (e.g., product -> category -> parentCategory -> name), the Content Lake has to perform more lookups per document.
To maintain performance, follow these guidelines:
- Avoid
*[]without filters: Never fetch all documents and then project; always filter by_typeor a specific ID first. - Prefer specific fields over
...: While the spread operator (...) is convenient for including all fields, explicitly naming your fields prevents your frontend from breaking when a CMS editor adds a large, unexpected field to the schema. - Limit dereferencing depth: If you find yourself going three or four levels deep, consider if your content model should be flattened or if a separate query is more appropriate.
Verifying Your Payload
To verify that your projections are working and reducing payload size, use the Network Tab in your browser's developer tools. Compare the response size of a query using *[_type == "product"] against one using a specific projection. You should see a significant reduction in the total kilobytes transferred, as the _createdAt, _updatedAt, and internal metadata are stripped away.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.