Ми часто бачимо, як команди обирають 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, щоб уникнути типових помилок.







