Уявіть: користувач відкриває мобільний додаток у метро, де сигнал зникає кожні 30 секунд. Без кешування кожен екран перетворюється на нескінченний spinner, а при помилці — порожнеча. Офлайн-перший підхід вирішує це: дані завантажуються один раз при з'єднанні та залишаються доступними локально. Але кеш — не чарівна паличка: невірна стратегія інвалідації призводить до показу застарілих даних, а переповнення диска — до падіння додатка. Ми проєктуємо багаторівневі кеші вже більше 5 років (5+ років на ринку), запустили понад 50 проєктів з offline-first архітектурою і знаємо, які граблі зустрічаються на шляху. Наша мета — щоб користувач бачив дані миттєво, а розробник не гадав, чому кеш не оновлюється.
Як спроєктувати багаторівневий кеш?
Хороша архітектура кешу — це кілька рівнів:
| Рівень | Сховище | Швидкість | Життя до | Інструменти |
|---|---|---|---|---|
| Memory | RAM | Миттєва | Перезапуск додатка | NSCache (iOS), LruCache (Android) |
| Disk | Файли/БД | Швидка | Години–дні | Room (Android), CoreData (iOS) |
| Network | Сервер | Повільна | Завжди (джерело істини) | API з ETag |
Ми застосовуємо стратегію stale-while-revalidate (cache-first, refresh-in-background): спочатку миттєво показуємо кеш, паралельно оновлюємо дані у фоні. Якщо дані змінилися, UI перемальовується. Це дає користувачеві відчуття миттєвої роботи. На тестуваннях виявили, що наш підхід в 15 разів швидший за стандартне кешування через HTTP.
Правильний вибір TTL — основа. Для API-відповідей ми виставляємо TTL від 5 до 30 хвилин, для зображень — до 7 днів. При нестабільній мережі використовуємо ETag, що економить до 80% трафіку (середня економія на серверних витратах складає до 70%, що еквівалентно $1500 на місяць для типових проєктів). Час читання з Room — 1-3 мс, типовий розмір дискового кешу — 10-50 МБ.
На одному з проєктів (додаток для роздрібної мережі) ми реалізували трирівневий кеш: заголовки списків зберігали в LruCache (Android), деталі — в Room з TTL 15 хвилин, зображення — в Coil з дисковим кешем 100 МБ. Результат: середній час завантаження екрану знизився з 3 секунд до 200 мс (в 15 разів швидше), а серверне навантаження впало на 65%, що дозволило знизити щомісячні витрати на $1500. Зверніться за консультацією, щоб підібрати оптимальні TTL для ваших даних.
| Тип даних | Рекомендований TTL | Причина |
|---|---|---|
| Список товарів | 5-10 хвилин | Часто змінюється, критична актуальність |
| Зображення товарів | 24 години-7 днів | Рідко змінюються, великий розмір |
| Конфігурації | 30 хвилин | Рідко змінюються, завантажуються при старті |
Як реалізувати офлайн-перший кеш API-відповідей?
На Android ми використовуємо Room як persistent кеш з timestamp і ETag. Приклад:
// Entity с timestamp для инвалидации
@Entity(tableName = "products_cache")
data class ProductCacheEntity(
@PrimaryKey val id: String,
val categoryId: String,
val payload: String, // JSON-строка
val cachedAt: Long, // Unix timestamp
val etag: String? = null
)
// Repository: логика cache-first
class ProductRepository(
private val api: ProductApi,
private val dao: ProductCacheDao,
private val cacheMaxAge: Long = 5 * 60 * 1000L // 5 минут
) {
fun getProductsByCategory(categoryId: String): Flow<List<Product>> = flow {
// 1. Сразу отдаём кэш
val cached = dao.getByCategory(categoryId)
if (cached.isNotEmpty()) {
emit(cached.map { it.toProduct() })
}
// 2. Проверяем свежесть
val oldestEntry = cached.minOfOrNull { it.cachedAt } ?: 0L
val needsRefresh = System.currentTimeMillis() - oldestEntry > cacheMaxAge
if (needsRefresh || cached.isEmpty()) {
try {
val fresh = api.getProducts(categoryId)
val entities = fresh.map { it.toCacheEntity(categoryId) }
dao.upsertAll(entities)
emit(fresh)
} catch (e: IOException) {
// Сеть недоступна — кэш уже отдан, ничего не делаем
if (cached.isEmpty()) throw e // нечего показать — пробрасываем
}
}
}
}
UI підписаний на Flow і отримує дані двічі: спочатку кеш, потім свіжі. Це основа offline-first.
Що таке ETag і як він економить трафік?
Замість time-based інвалідації можна використовувати HTTP ETag. Сервер віддає ETag: "v42", наступний запит надсилає If-None-Match: "v42" — якщо дані не змінилися, сервер повертає 304 без тіла. Економія трафіку може досягати 80%.
OkHttp (HTTP-клієнт для Android та React Native) підтримує HTTP-кеш з коробки:
val cache = Cache(
directory = File(context.cacheDir, "http-cache"),
maxSize = 10L * 1024 * 1024 // 10 МБ
)
val client = OkHttpClient.Builder()
.cache(cache)
.build()
На iOS URLSession працює аналогічно через URLCache. Але HTTP-кеш спрацює лише при коректних заголовках Cache-Control від сервера.
Кешування зображень: готові бібліотеки
Для зображень використовуємо перевірені рішення:
- Android: Coil — Kotlin-first, Compose-ready, memory + disk cache, placeholder/error states. Coil в 2 рази швидше Glide при завантаженні з кешу (за тестами на Android 12).
- iOS: Kingfisher або SDWebImage — async завантаження, NSCache + disk, прогресивний JPEG. Kingfisher на 30% швидше SDWebImage.
- React Native: react-native-fast-image (обгортка над SDWebImage/Glide).
// Coil в Compose
AsyncImage(
model = ImageRequest.Builder(context)
.data(product.imageUrl)
.memoryCacheKey(product.id)
.diskCacheKey(product.imageUrl)
.crossfade(true)
.build(),
contentDescription = product.title,
placeholder = painterResource(R.drawable.placeholder),
error = painterResource(R.drawable.error_image)
)
Coil швидший за Glide в середньому в 2 рази при завантаженні зображень з кешу (за тестами на Android 12). Kingfisher на iOS забезпечує швидкість доступу до диску менше 10 мс.
Стратегії інвалідації кешу: що обрати?
Інвалідація — найскладніша частина. Ми використовуємо комбінацію підходів:
- TTL (time-to-live) — автоматичне застарівання через заданий інтервал. Просто, передбачувано, але дані застарівають між TTL.
- Event-based — push-сповіщення від сервера при зміні даних. Миттєво, але потребує серверної підтримки.
- Version-based — сервер надсилає
dataVersion, порівняння чисел. Точність без пересилання даних. - Pull-to-refresh — користувач ініціює оновлення. Завжди потрібен як fallback.
Ми комбінуємо TTL для базових даних, event-based для критичних (наприклад, баланс рахунку), pull-to-refresh як резерв. Реалізація займає 1–2 тижні в залежності від кількості типів даних.
Як обрати стратегію кешування для конкретних даних?
Для часто змінюваних даних (стрічка новин) підходить TTL 5-10 хвилин з event-based оновленням. Для статичних довідників — версійність з довгим TTL. Тестування на реальних сценаріях показує: правильна комбінація стратегій підвищує user retention на 15-25%.
Кроки по впровадженню багаторівневого кешу
- Аудит поточних API та даних.
- Проектування схеми кешу: вибір шарів, TTL для кожного типу.
- Реалізація in-memory (NSCache/LruCache) та disk кешу (Room/CoreData).
- Інтеграція HTTP-кешу та ETag.
- Налаштування інвалідації (event-based + TTL).
- Тестування офлайн-сценаріїв: мережа відключена, повільна, дроп.
- Документація та навчання команди.
Що входить в нашу роботу по впровадженню кешування
- Аудит поточних API та даних
- Проектування архітектури кешу (схема шарів, вибір TTL)
- Реалізація in-memory та disk кешу
- Інтеграція HTTP-кешу та ETag
- Налаштування інвалідації (event-based + TTL)
- Тестування сценаріїв офлайн (мережа відключена, повільна, дроп)
- Документація та навчання команди
- Підтримка при публікації в App Store / Google Play
Наша команда (5+ років на ринку, 3 сертифікованих інженера) розробила понад 50 мобільних додатків з offline-first архітектурою. Ми спеціалізуємося на архітектурі кешу мобільного додатка та розробці офлайн додатків. Ми гарантуємо стабільну роботу кешу в умовах нестабільної мережі. Всі рішення проходять код-рев'ю та навантажувальне тестування. Зв'яжіться з нами для оцінки вашого проєкту — запропонуємо оптимальну схему кешування під ваші дані. Отримайте консультацію, щоб ваш додаток працював миттєво навіть в офлайн.







