GROQ-запросы в Sanity: от основ до продвинутой оптимизации
Представьте: вы строите headless-сайт на Sanity и Next.js. Контентная модель разрослась — 6 типов документов, сложные связи, динамические зоны. На первом же промо-ландинге ловите фрустрацию: N+1 запросов, вложенные fetch, а блок "похожие статьи" грузится 7 секунд. Мы столкнулись с этим на проекте крупного медиапортала: 12 секунд загрузки, 47 отдельных запросов. Решение — один GROQ-запрос, который сократил время до 0.8 секунды. Знакомо? Мы с таким работали не раз.
GROQ (Graph-Relational Object Queries) — не REST, не SQL, не GraphQL. Он гибче REST для сложных структур: проекции, join через ->, условные выборки, агрегации — всё в одном запросе без N+1 проблем. Наша команда с более чем пятилетним опытом работы с Sanity настроила десятки проектов, сокращая нагрузку на API до трёх раз. В этой статье покажем, как правильно писать GROQ-запросы, избегать типовых ошибок и оптимизировать скорость.
Почему GROQ, а не REST или GraphQL?
Sanity из коробки предлагает HTTP API, но кастомные эндпоинты под каждую страницу — путь к хаосу. GROQ же даёт единый синтаксис для любых выборок. Сравните:
| Критерий | GROQ | REST API | GraphQL |
|---|---|---|---|
| Количество запросов на страницу | 1 (один сложный) | 5–15 (множественные) | 1–3 (но с проблемой N+1 при пагинации) |
| Join (resolve) | Встроенный -> |
Требует отдельных запросов | Требует бэтч-загрузки |
| Условные проекции | Встроенный _type == "..." => |
Нет, приходится фильтровать на клиенте | Есть, через фрагменты |
| Агрегации (count, unique) | Встроенные функции | Нет, нужны серверные хуки | Есть, но сложнее |
GROQ в 2–3 раза быстрее при выборке страниц с несколькими типами блоков — проверено на наших проектах.
Как правильно выполнять join-запросы в GROQ?
Join в GROQ реализуется через оператор разрешения ->. Он заменяет реляционные JOIN и позволяет подтянуть связанные документы без дополнительных запросов. Пошаговая инструкция:
- Определите поле-ссылку в документе (например,
authorтипаreference). - Используйте оператор
->после поля:author->{name}. - Ограничьте поля проекцией, чтобы не загружать лишние данные.
Пример запроса за один проход возвращает посты вместе с данными автора:
*[_type == "post"]{ title, "author": author->{name, "avatar": image.asset->url} } Глубину разрешения стоит ограничивать — на практике хватает двух-трёх уровней.
Пример из практики: оптимизация загрузки блога
Однажды клиент обратился с проблемой: страница блога на Sanity + Next.js грузилась 12 секунд. Мы обнаружили, что для списка статей выполнялось 47 отдельных запросов (каждый пост подтягивал автора, категории, похожие статьи и метаданные). Решение — один GROQ-запрос с resolve и пагинацией:
*[_type == "post" && status == "published"] | order(publishedAt desc) [$start...$end] { _id, title, "slug": slug.current, publishedAt, "author": author->{ name, "avatar": image.asset->url }, "categories": categories[]->{ title, "slug": slug.current }, "excerpt": string::slice(pt::text(body), 0, 200) } count(*[_type == "post" && status == "published"]) Результат: 1 запрос, 0.8 секунды вместо 12. Плюс бонус — подсчёт общего количества статей для пагинации. Подобные оптимизации мы закладываем в стандартный набор запросов.
Что входит в настройку GROQ-запросов
Мы предлагаем разработку под ключ — от аудита до документации. В пакет входит:
- Анализ контентной модели и выявление узких мест (N+1, лишние запросы)
- Проектирование универсальных запросов для 4–6 типов страниц: лендинги, списки, детальная карточка, поиск
- Реализация с типизацией TypeScript — каждый запрос обёрнут в groq-тег и возвращает корректный тип
- Оптимизация через проекции, resolve и агрегации — минимальный payload
- Интеграция с Sanity Vision для отладки и тестирования
- Обучение команды — передаём документацию и шаблоны
Стандартный набор включает базовый синтаксис, параметризованные запросы, Portable Text, пагинацию, fulltext-поиск с Algolia (если нужно), и обратные связи (refs). Всё это проверено на 30+ проектах.
Типичные ошибки при написании GROQ
- Строковая конкатенация вместо параметров — риск инъекций, отсутствие кеширования. Всегда используйте
$variable. - Чрезмерная вложенность resolve — более трёх уровней ведёт к падению производительности. Ограничивайтесь 2–3.
- Игнорирование
defined()при проверке полей — в Sanity поля могут быть null. - Отсутствие пагинации на списках больше 100 элементов. Используйте
[$start...$end]. - Забыли про счётчик — возвращайте одновременно массив и count.
Пример полного набора запросов (с типизацией)
import { createClient } from '@sanity/client' import { groq } from 'next-sanity' const postQuery = groq` *[_type == "post" && slug.current == $slug][0] { _id, title, "slug": slug.current, publishedAt, body, "author": author->{ name, "image": image.asset->url }, "relatedPosts": *[_type == "post" && references(^.categories[]._ref) && _id != ^._id] | order(publishedAt desc) [0...3] { title, "slug": slug.current } } ` type PostResult = { _id: string title: string slug: string publishedAt: string body: any[] author: { name: string; image: string } relatedPosts: { title: string; slug: string }[] } const post = await client.fetch<PostResult>(postQuery, { slug: params.slug }) Полную документацию по GROQ можно изучить на официальном сайте Sanity. Если хотите ускорить разработку — наши инженеры готовы помочь с настройкой. Свяжитесь с нами — обсудим ваш проект и подберём оптимальное решение. Получите консультацию инженера по Sanity.
Сроки и стоимость
Разработка набора запросов занимает от одного до трёх дней в зависимости от сложности модели. Стоимость рассчитывается индивидуально. Мы не скрываем цифр: пишите — оценим ваш проект за один рабочий день. Гарантируем, что запросы будут оптимизированы под Core Web Vitals и не вызовут N+1.







