Мы часто видим, как команды выбирают Firestore за real-time возможности и масштабируемость, но допускают ошибки в структуре данных или подписках. Например, один стартап с социальной сетью после запуска получил задержки до 2 секунд: причина — неправильная организация подколлекций и отсутствие лимитов на onSnapshot. Мы перепроектировали схему, добавили пагинацию с get() и сократили задержки до 100 мс. Заодно снизили ежемесячный счет за Firebase на 30% за счет уменьшения операций чтения.
За 5+ лет и 30+ проектов мы наработали проверенные решения, которые обеспечивают стабильную работу даже при 10 000 одновременных пользователей. В этой статье разберём ключевые приёмы: от структуры данных до транзакций и правил безопасности. Вы узнаете, как избежать типичных ошибок и оптимизировать затраты.
Правильная структура данных
Firestore — документно-ориентированная БД. В отличие от Realtime Database, она поддерживает составные индексы и сложные запросы. Но её ограничения (максимум 1 МБ на документ, 20 полей в составном индексе) требуют продуманной схемы.
Для социального приложения типичная структура:
users/{userId} ├── displayName: string ├── photoURL: string └── posts/{postId} ← подколлекция ├── text: string ├── createdAt: Timestamp └── likes: number Подколлекции — единственный правильный способ хранить неограниченно растущие данные. Избегайте вложенных массивов: они увеличивают размер документа и приводят к лишнему чтению. Также рекомендуется использовать ссылки на документы вместо вложенных объектов.
Как настроить подписки и пагинацию?
Для real-time ленты мы используем подписку только на первую страницу, а последующие подгружаем через get() с пагинацией. Это предотвращает разрастание снапшота — при 1000 подписчиков экономия до 90% трафика.
import firestore from '@react-native-firebase/firestore'; const [posts, setPosts] = useState<Post[]>([]); const [lastDoc, setLastDoc] = useState<DocumentSnapshot | null>(null); // Real-time: только первые 10 постов useEffect(() => { const unsubscribe = firestore() .collection(`users/${userId}/posts`) .orderBy('createdAt', 'desc') .limit(10) .onSnapshot(snapshot => { const fresh = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() } as Post)); setPosts(prev => { const ids = new Set(fresh.map(p => p.id)); return [...fresh, ...prev.filter(p => !ids.has(p.id))]; }); }); return () => unsubscribe(); }, [userId]); // Load more через get() const loadMore = useCallback(async () => { let query = firestore() .collection(`users/${userId}/posts`) .orderBy('createdAt', 'desc') .limit(20); if (lastDoc) query = query.startAfter(lastDoc); const snapshot = await query.get(); const newPosts = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() } as Post)); setPosts(prev => [...prev, ...newPosts]); setLastDoc(snapshot.docs[snapshot.docs.length - 1] ?? null); }, [lastDoc, userId]); Антипаттерн: вешать onSnapshot на весь запрос с пагинацией — каждое изменение в коллекции возвращает полный снимок первых N документов, что замедляет UI.
Настройка офлайн-кэша
Для включения офлайн-кэша достаточно настроить параметры: persistence: true и cacheSizeBytes. Как указано в документации Firebase, офлайн-кэш включен по умолчанию, но можно задать размер кэша, например, 100 МБ. При офлайне onSnapshot читает из кэша, а записи буферизуются. Это снижает количество платных операций чтения — экономия до 30% ежемесячного счета.
Планирование индексов
Запрос с where + orderBy по разным полям требует составного индекса. Firestore предлагает создать его при первой ошибке, но в продакшне лучше определить индексы в firestore.indexes.json до деплоя:
{ "indexes": [ { "collectionGroup": "posts", "queryScope": "COLLECTION", "fields": [ { "fieldPath": "userId", "order": "ASCENDING" }, { "fieldPath": "createdAt", "order": "DESCENDING" } ] } ] } Создание индекса может занять от нескольких минут до часов на больших коллекциях. Без него вы получите FAILED_PRECONDITION. Планируйте индексы на этапе проектирования — это сэкономит время.
Правила безопасности для защиты данных
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /users/{userId}/posts/{postId} { allow read: if request.auth != null; allow write: if request.auth.uid == userId && request.resource.data.keys().hasAll(['text', 'createdAt']) && request.resource.data.text is string && request.resource.data.text.size() <= 2000; } } } Никогда не полагайтесь только на клиентскую валидацию — правила безопасности должны проверять типы, размеры и права доступа. Это первый рубеж защиты. Дополнительно настройте мониторинг аномальных запросов через Firebase Extensions.
Почему стоит использовать транзакции?
Транзакции гарантируют атомарность операций. Например, при лайке поста нужно проверить, не поставил ли пользователь лайк ранее, и атомарно увеличить/уменьшить счетчик.
await firestore().runTransaction(async transaction => { const postRef = firestore().doc(`posts/${postId}`); const userLikeRef = firestore().doc(`userLikes/${userId}_${postId}`); const [postSnap, likeSnap] = await Promise.all([ transaction.get(postRef), transaction.get(userLikeRef), ]); if (likeSnap.exists()) { transaction.delete(userLikeRef); transaction.update(postRef, { likes: firestore.FieldValue.increment(-1) }); } else { transaction.set(userLikeRef, { userId, postId, createdAt: firestore.FieldValue.serverTimestamp() }); transaction.update(postRef, { likes: firestore.FieldValue.increment(1) }); } }); Используйте FieldValue.increment() для атомарного обновления счётчиков — это быстрее и безопаснее, чем read-modify-write. Транзакции критичны для консистентности при высокой нагрузке, например при 1000 лайков в минуту.
Что входит в работу
При заказе интеграции Firestore в ваше мобильное приложение мы предоставляем:
- Аудит текущей схемы данных и поиск узких мест (типичные проблемы: дублирующиеся коллекции, избыточные индексы, неправильные правила безопасности).
- Проектирование новых коллекций, индексов и правил безопасности с учётом планов роста (до 100 000 пользователей).
- Реализацию real-time подписок с пагинацией и офлайн-кэшем на всех платформах (iOS, Android, Flutter, React Native).
- Покрытие критичных операций транзакциями (лайки, покупки, заявки).
- Нагрузочное тестирование до 10 000 RPS и оптимизацию запросов — среднее снижение операций чтения на 40%.
- Подготовку документации по архитектуре и настройкам, а также обучение команды (2–3 часа с ответами на вопросы).
- Поддержку в течение 2 недель после деплоя для решения возможных проблем.
Процесс работы
- Аудит текущей схемы данных — анализ структуры, поиск узких мест.
- Проектирование — разработка оптимальной модели, настройка индексов и правил безопасности.
- Реализация — интеграция с iOS (Swift), Android (Kotlin), Flutter или React Native, настройка подписок и пагинации.
- Тестирование — нагрузочное тестирование до 10 000 RPS, проверка офлайн-режима.
- Деплой и документация — миграция данных, настройка мониторинга, код-ревью.
Мы используем стек: Swift 5.9+, Kotlin с Coroutines, Flutter 3.x, React Native с TypeScript, Apollo GraphQL, Firebase Extensions.
Сравнение Firestore и Realtime Database
| Критерий | Firestore | Realtime Database |
|---|---|---|
| Тип данных | Документно-ориентированная | JSON-дерево |
| Запросы | Составные индексы, where, orderBy | Фильтрация по ключам |
| Масштабирование | Автоматическое, до 1 млн записей | Требует шардинга |
| Офлайн-кэш | Да, с настраиваемым размером | Да, но более ограничен |
| Цена | За операции чтения/записи | За трафик и хранение |
Firestore лучше подходит для приложений со сложными запросами и необходимостью real-time обновлений. RTDB — для простых синхронизаций.
Сроки и ориентиры
| Этап | Длительность |
|---|---|
| Анализ | 1-2 дня |
| Проектирование | 1 день |
| Реализация | 5-15 дней |
| Тестирование | 2 дня |
| Деплой | 1 день |
Ориентировочные сроки: от 2 до 4 недель. Стоимость рассчитывается индивидуально после оценки объёма работ.
Свяжитесь с нами для аудита вашей схемы данных — наши инженеры помогут выбрать оптимальную архитектуру и снизить расходы на Firebase. Закажите консультацию по интеграции Firestore, чтобы избежать типичных ошибок.







