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-а који одмах израчунава layout свих подређених елемената, 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-у који не заузима место у layout-у и не креира подређене 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 — за прилагођене 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. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође