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 буфера), започва неговото зареждане. Когато елемент напусне екрана, ресурсите му могат да бъдат освободени или преместени в кеша.
// 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 елемента < 5 элементов
if (lastVisible >= totalCount - 5) {
loadMoreItems()
}
}
})
Prefetch буфер — предварително зареждане на елементи, които скоро ще се появят на екрана. RecyclerView поддържа GapWorker.Prefetch чрез layoutManager.setItemPrefetchEnabled(true). iOS UITableView поддържа prefetching чрез UITableViewDataSourcePrefetching. Размерът на prefetch буфера обикновено е 1–2 екрана напред, което дава компромис между гладкостта на скролването и консумацията на памет. Твърде голям prefetch буфер унищожава предимствата на Lazy Loading, твърде малък — създава празни места при бързо скролване.
Изображенията — най-тежкият тип ресурси в мобилните приложения. Една снимка от 12 MP може да заема 3–5 MB в некомпресиран вид. Lazy Loading на изображения предотвратява зареждането на стотици невидими картинки в паметта, което би било фатално за списъци с потребителски коментари или каталози с продукти.
Glide — най-популярната библиотека за зареждане на изображения за Android с поддръжка на кеширане, трансформации и анимации. Coil — по-лека алтернатива, написана на Kotlin с използване на корутини. И двете библиотеки автоматично спират зареждането, когато ImageView напусне екрана, и отменят заявките при повторно използване на ViewHolder. Coil използва корутини и е с размер ~1.5 MB срещу ~4 MB за 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, който незабавно изчислява оформлението на всички дъщерни елементи, LazyVStack отлага създаването на изглед до момента, когато елементът стане видим или влезе в prefetch диапазона. LazyHStack — хоризонталният аналог за карусели. За списъци с голям обем Apple препоръчва използването на List, който вътрешно работи подобно на LazyVStack с допълнително вградено рециклиране.
// SwiftUI — LazyVStack с мързеливо зареждане
ScrollView {
LazyVStack(spacing: 8) {
ForEach(articles) { article in
ArticleRow(article: article)
.onAppear {
if article == articles.last {
viewModel.loadMore()
}
}
}
}
}
Мързеливо зареждане на UI компоненти — техника, при която части от интерфейса (хедъри, футъри, секции с настройки, табове) се създават не при стартиране на екрана, а при първия достъп до тях. Това ускорява първоначалното рендериране и намалява натоварването на main thread.
ViewStub — лек заместител на View в Android, който не заема място в оформлението и не създава дъщерни View-та до извикването на inflate(). Идеален за рядко използвани секции: панел за търсене, разширени настройки, рекламни блокове. Мързеливо зареждане на Fragment — техника, при която 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 ms дори на бюджетни устройства с 3 GB RAM.
Често задавани въпроси
Ако екранът гарантирано показва малко елементи (до 20) и всички са необходими веднага — Lazy Loading е излишен. За списъци, които рядко се скролват, Eager Loading може да бъде по-прост и по-бърз за имплементация без забележима загуба на производителност.
Намалява пиковата консумация на памет 3–10 пъти за големи списъци, тъй като в паметта се съхраняват само видимите елементи плюс prefetch буфера. Добавянето на prefetch и кеширането на изображения обаче създава умерена консумация на памет, която трябва да се управлява.
List е за предпочитане за хомогенни данни с възможност за плъзгане, влачене и вградена селекция. LazyVStack — за персонализирани оформления с различни типове клетки, секции и нестандартни разстояния. 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 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също