Lazy Loading — стратегія відкладеного завантаження даних, зображень і компонентів, за якої ресурси запитуються не під час запуску застосунку, а в момент, коли вони реально знадобилися користувачеві. За даними Android Paging 3 Guide, ліниве завантаження списків знижує споживання пам'яті на 60–80% під час роботи з великими наборами даних. Відкладена ініціалізація — ключовий принцип, що лежить в основі всіх реалізацій Lazy Loading.
Головне
Lazy Loading (ліниве завантаження, відкладене завантаження) — патерн проєктування та оптимізації, за якого ресурси застосунку завантажуються не під час запуску, а безпосередньо перед використанням. У мобільній розробці Lazy Loading застосовується до трьох основних категорій: дані (пагінація списків), зображення (завантаження під час скролу) та компоненти (ліниві стеки та в'ю).
Протилежність Lazy Loading — Eager Loading (жадібне завантаження), коли всі ресурси завантажуються під час запуску екрана. Eager Loading простіший у реалізації, але споживає більше пам'яті та збільшує час першого відображення. Для списків із тисячами елементів Eager Loading призводить до OOM (Out of Memory) на пристроях з обмеженою пам'яттю. Lazy Loading вирішує цю проблему, завантажуючи лише те, що видно на екрані, і підвантажуючи решту під час скролу.
У контексті iOS та Android Lazy Loading реалізовано на різних рівнях. SwiftUI надає LazyVStack і LazyHStack для лінивого рендерингу. UIKit використовує UITableView з dequeueReusableCell. Android — RecyclerView з пулом ViewHolder. На рівні даних — Room з Paging 3 і Core Data з NSFetchedResultsController. Вибір конкретної технології залежить від стека та вимог до продуктивності.
Центральний принцип Lazy Loading — завантаження рівно того обсягу даних, який необхідний для поточного стану екрана, плюс випереджувальний буфер для плавного скролу. Цей підхід базується на двох механізмах: відстеження видимості та віртуалізація елементів.
Механізм відстеження визначає, які елементи знаходяться у видимій області екрана (viewport). На Android це робить LinearLayoutManager або GridLayoutManager через методи findFirstVisibleItemPosition і findLastVisibleItemPosition. В iOS UIScrollView надає bounds.origin.y і contentOffset.height для розрахунку видимої області. Коли елемент потрапляє у viewport (або prefetch buffer), запускається його завантаження. Коли елемент покидає екран, його ресурси можуть бути звільнені або переміщені в кеш.
// Android — відстеження видимості в RecyclerView
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
val lastVisible = layoutManager.findLastVisibleItemPosition()
val totalCount = layoutManager.itemCount
// Завантажуємо наступну сторінку, якщо залишилося < 5 элементов
if (lastVisible >= totalCount - 5) {
loadMoreItems()
}
}
})
Prefetch buffer — випереджувальне завантаження елементів, які скоро з'являться на екрані. RecyclerView підтримує GapWorker.Prefetch через layoutManager.setItemPrefetchEnabled(true). iOS UITableView підтримує prefetching через UITableViewDataSourcePrefetching. Розмір prefetch buffer зазвичай становить 1–2 екрани вперед, що дає компроміс між плавністю скролу та споживанням пам'яті. Завеликий prefetch buffer зводить нанівець переваги Lazy Loading, занадто малий — створює порожні місця під час швидкого скролу.
Зображення — найважчий тип ресурсів у мобільних застосунках. Одне фото в 12 МП може займати 3–5 МБ у нестисненому вигляді. Lazy Loading зображень запобігає завантаженню сотень невидимих картинок у пам'ять, що було б фатально для списків з аватарками користувачів або каталогів товарів.
Glide — найпопулярніша бібліотека завантаження зображень для Android із підтримкою кешування, трансформацій та анімацій. Coil — легша альтернатива, написана на Kotlin з використанням корутин. Обидві бібліотеки автоматично призупиняють завантаження, коли ImageView покидає екран, і скасовують запити при повторному використанні ViewHolder. Coil використовує корутини та має розмір ~1.5 МБ проти ~4 МБ у Glide, що робить його кращим вибором для проєктів з фокусом на розмір APK.
// Coil — ліниве завантаження зображення
imageView.load("https://example.com/image.jpg") {
crossfade(true)
placeholder(R.drawable.placeholder)
size(512, 512)
memoryCachePolicy(CachePolicy.ENABLED)
}
Kingfisher — бібліотека для iOS із підтримкою Swift Concurrency, кешування на диск та в пам'ять, а також prefetching для UICollectionView. SDWebImage — старіша бібліотека з корінням в Objective-C, але з підтримкою Swift. Обидві бібліотеки інтегруються з UIImageView та автоматично керують життєвим циклом завантаження: скасовують запити при перевикористанні комірки, завантажують зображення лише коли комірка видима та звільняють пам'ять при повідомленні про нестачу пам'яті.
Великі списки даних — основна сфера застосування Lazy Loading у мобільних застосунках. Стрічка новин, каталог товарів, чати, історія операцій — будь-який екран з потенційно нескінченним списком потребує пагінації та відкладеного завантаження.
Paging 3 — бібліотека з Android Jetpack, що реалізує повний цикл лінивого завантаження: запит даних з RemoteMediator (API + БД), кешування в Room, посторінкова видача через PagingData та відображення через AsyncPagingDataAdapter. Paging 3 підтримує три типи пагінації: Page-based (сторінки), Item-based (offset/limit) та Key-based (ключі пагінації від API). Separator — вбудована підтримка розділювачів між сторінками для індикаторів завантаження.
// Paging 3 — ліниве завантаження з API
class ArticlePagingSource(
private val api: ArticleApi
) : PagingSource<Int, Article>() {
override suspend fun load(
params: LoadParams<Int>
): LoadResult<Int, Article> {
return try {
val page = params.key ?: 1
val response = api.getArticles(page)
LoadResult.Page(
data = response.items,
prevKey = page.takeIf { it > 1 }?.dec(),
nextKey = page + 1
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
}
LazyVStack — вбудований контейнер SwiftUI, який створює та рендерить елементи лише при їх появі на екрані. На відміну від VStack, який одразу обчислює layout всіх дочірніх елементів, LazyVStack відкладає створення view до моменту, коли елемент стає видимим або потрапляє в prefetch range. LazyHStack — горизонтальний аналог для каруселей. Для списків великого обсягу Apple рекомендує використовувати List, який всередині працює аналогічно LazyVStack з додатковим вбудованим recycling.
// SwiftUI — LazyVStack з лінивим завантаженням
ScrollView {
LazyVStack(spacing: 8) {
ForEach(articles) { article in
ArticleRow(article: article)
.onAppear {
if article == articles.last {
viewModel.loadMore()
}
}
}
}
}
Ліниве завантаження UI-компонентів — техніка, за якої частини інтерфейсу (хідери, футери, секції налаштувань, вкладки) створюються не під час запуску екрана, а при першому зверненні до них. Це пришвидшує initial render та знижує навантаження на main thread.
ViewStub — легкий View-заповнювач в Android, який не займає місця в layout і не створює дочірніх View до виклику inflate(). Ідеальний для рідко використовуваних секцій: панель пошуку, розширені налаштування, рекламні блоки. Fragment lazy loading — техніка, за якої Fragment.onCreateView відкладається до моменту, коли користувач перемкнеться на цю вкладку. Реалізується через isVisible або UserVisibleHint у ViewPager.
AndroidX надає SplitInstallManager для лінивого завантаження модулів на вимогу. Модулі налаштування, діагностики або додаткових функцій завантажуються як Dynamic Feature модулі лише при першому запиті користувача. Це зменшує базовий розмір застосунку на 30–50% і одночасно реалізує принцип Lazy Loading на рівні не лише даних, але й коду.
TabView в SwiftUI завантажує вміст кожної вкладки ліниво — лише при активації таба. UIKit UITabBarController за замовчуванням створює всі дочірні контролери під час запуску, але цю поведінку можна змінити, не додаючи їх у tabBarController.viewControllers одразу, а підставляючи в міру перемикання. UIStackView з arrangedSubviews, що додаються динамічно, також слідує принципу Lazy Loading — додавайте Subview лише коли користувач виконує дію, що потребує цієї частини інтерфейсу.
Для оптимізації завантаження екрана в цілому комбінуйте Lazy Loading на всіх рівнях: ViewStub для рідко використовуваних секцій, Paging 3 для даних, Glide/Coil для зображень і лінива ініціалізація ViewModel через Hilt/Dagger Scopes або Swinject. Такий підхід дає екран, який завантажується за 200–400 мс навіть на бюджетних пристроях з 3 ГБ оперативної пам'яті.
Часті запитання
Якщо екран гарантовано показує мало елементів (до 20) і всі вони потрібні одразу — Lazy Loading зайвий. Для списків, які рідко скроляться, Eager Loading може бути простішим і швидшим у реалізації без помітних втрат продуктивності.
Знижує пікове споживання пам'яті в 3–10 разів для великих списків, оскільки в пам'яті зберігаються лише видимі елементи плюс prefetch buffer. Однак додавання prefetch і кешування зображень створює помірну витрату пам'яті, якою потрібно керувати.
List краще підходить для однорідних даних із можливістю свайпу, перетягування та вбудованого вибору. LazyVStack — для кастомних layout із різними типами комірок, секціями та нестандартними відступами. List всередині працює як LazyVStack із додатковою функціональністю.
В Android використовуйте Layout Inspector для перегляду ієрархії View: при Lazy Loading більша частина елементів повинна бути відсутня в дереві. В iOS — Xcode Debug View Hierarchy. Якщо під час скролу всі елементи присутні — Lazy Loading не працює.
Обидва параметри важливі, але пріоритет залежить від платформи. На iOS з ARC та ефективним керуванням пам'яттю пріоритет — швидкість завантаження. На Android з JVM та GC — пріоритет — економія пам'яті, оскільки кожна алокація в main thread може викликати GC-фриз.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також