
Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag storeLooking at your queries, there are several performance issues that could explain the 4-8 second response times. Let me walk through the problems and solutions:
1. N+1 Query Problem in getAllSubCatgoriesVideos
This is your biggest performance killer:
const subCategoriesWithVideos = await Promise.all(
filteredForGrade.map(async (subcategory) => {
const videos = await getFilteredVideos(...); // Separate query for EACH subcategory!
return { ...subcategory, videos };
})
);If you have 10 subcategories, you're making 11 separate queries (1 for subcategories + 10 for videos). This compounds the latency. Instead, fetch everything in one query using GROQ projections:
export async function getAllSubCatgoriesVideos(topic, grade) {
const results = await client.fetch(
`*[_type == 'videoSubCategory'
&& category->title == $topic
&& category->grade->label == $grade] {
title,
category->{title, 'grade': grade->label},
'videos': *[_type == 'video'
&& type->name == "Concept Video"
&& references(^._id)
&& $grade in grade[]->label
&& $topic in category[]->title][0...3]{${videoFields}}
} | order(title asc)`,
{ topic, grade }
);
return results;
}2. Client-Side Filtering Instead of GROQ
In getAllSubCategories, you're fetching all subcategories then filtering in JavaScript:
const filteredForGrade = results.filter((subtopic) => {
return subtopic.category.grade === grade;
});This forces Sanity to return unnecessary data. Move the filter into your GROQ query as shown in the example above with category->grade->label == $grade.
3. Not Using Query Parameters
Your string concatenation approach has security and caching implications. According to the GROQ parameters documentation, using parameters prevents injection issues and allows better query caching:
// Current approach - works but not ideal
query += `[category.title == "${topic}"]`;
// Better approach with parameters
const results = await client.fetch(
`*[_type == "video" && $topic in category[]->title]{...}`,
{ topic: filterCategory }
);Note: Your current filter chaining syntax (like *[_type == "video"]{...}[type.name == "..."]) is actually valid GROQ according to the pipeline components documentation - filters can be chained after projections. However, it's generally more efficient to combine filters before the projection using &&.
Here's a more performant version of getFilteredVideos:
export async function getFilteredVideos(
limit,
filterGrade,
filterCategory,
search,
sort,
type = "Lesson Video",
subCategory
) {
const filters = ['_type == "video"', 'type->name == $type'];
const params = { type };
if (filterGrade && filterGrade !== "All Grades") {
filters.push('$grade in grade[]->label');
params.grade = filterGrade;
}
if (filterCategory && filterCategory !== "All Categories") {
filters.push('$category in category[]->title');
params.category = filterCategory;
}
if (subCategory && subCategory !== "All Subtopic") {
filters.push('subCategory->title match $subCategory');
params.subCategory = subCategory;
}
if (search && search !== "All") {
filters.push('name match $search');
params.search = `${search}*`;
}
let query = `*[${filters.join(' && ')}]{${videoFields}}`;
if (sort === "longest") query += ' | order(duration desc)';
if (sort === "shortest") query += ' | order(duration asc)';
if (sort === "relevance") query += ' | order(type->name desc)';
query += ` [0...${limit}]`;
const results = await client.fetch(query, params);
return results;
}Based on the query optimization guide:
Check your dataset size: 4-8 seconds suggests either a very large dataset or other issues. How many video documents do you have?
Enable API CDN: Make sure you're using the API CDN which caches responses. Use useCdn: true in your client config:
const client = createClient({
// ... other config
useCdn: true,
});Use the published perspective: If you don't need draft content, the published perspective is faster than raw.
Test in Vision plugin: Use the Vision plugin in your Studio to test query performance directly and see execution times.
With these changes—especially fixing the N+1 problem—you should see dramatic improvements. The single biggest win will be consolidating those multiple queries into one.
Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.
Content operations
Content backend


The only platform powering content operations
By Industry


Tecovas strengthens their customer connections
Build and Share

Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag store