Lazy Loading is a strategy for deferred loading of data, images, and components, where resources are requested not at app startup, but at the moment when they are actually needed by the user. According to the Android Paging 3 Guide, lazy loading of lists reduces memory consumption by 60–80% when working with large datasets. Deferred initialization is the key principle underlying all Lazy Loading implementations.
Key Takeaways
Lazy Loading is a design and optimization pattern in which application resources are loaded not at startup, but immediately before use. In mobile development, Lazy Loading applies to three main categories: data (list pagination), images (loading on scroll), and components (lazy stacks and views).
The opposite of Lazy Loading is Eager Loading, where all resources are loaded at screen startup. Eager Loading is simpler to implement but consumes more memory and increases the time to first render. For lists with thousands of items, Eager Loading leads to OOM (Out of Memory) on devices with limited memory. Lazy Loading solves this problem by loading only what is visible on screen and loading the rest as the user scrolls.
In the context of iOS and Android, Lazy Loading is implemented at different levels. SwiftUI provides LazyVStack and LazyHStack for lazy rendering. UIKit uses UITableView with dequeueReusableCell. Android uses RecyclerView with a ViewHolder pool. At the data level, Room with Paging 3 and Core Data with NSFetchedResultsController. The choice of specific technology depends on the stack and performance requirements.
The central principle of Lazy Loading is loading exactly the amount of data that is needed for the current screen state, plus an anticipatory buffer for smooth scrolling. This approach is based on two mechanisms: visibility tracking and element virtualization.
The tracking mechanism determines which elements are in the visible area of the screen (viewport). On Android, this is done by LinearLayoutManager or GridLayoutManager through the findFirstVisibleItemPosition and findLastVisibleItemPosition methods. In iOS, UIScrollView provides bounds.origin.y and contentOffset.height for calculating the visible area. When an element enters the viewport (or the prefetch buffer), its loading is triggered. When an element leaves the screen, its resources can be released or moved to the cache.
// Android — tracking visibility in 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
// Loading the next page if there are < 5 items
if (lastVisible >= totalCount - 5) {
loadMoreItems()
}
}
})
Prefetch buffer is anticipatory loading of elements that will soon appear on screen. RecyclerView supports GapWorker.Prefetch via layoutManager.setItemPrefetchEnabled(true). iOS UITableView supports prefetching through UITableViewDataSourcePrefetching. The prefetch buffer size is typically 1–2 screens ahead, providing a compromise between scroll smoothness and memory consumption. A too large prefetch buffer negates the advantages of Lazy Loading; too small a buffer creates blank spaces during fast scrolling.
Images are the heaviest type of resource in mobile applications. A single 12 MP photo can take 3–5 MB in uncompressed form. Lazy Loading of images prevents loading hundreds of invisible pictures into memory, which would be fatal for lists with user avatars or product catalogs.
Glide is the most popular image loading library for Android with support for caching, transformations, and animations. Coil is a lighter alternative, written in Kotlin using coroutines. Both libraries automatically pause loading when an ImageView leaves the screen and cancel requests when a ViewHolder is reused. Coil uses coroutines and is ~1.5 MB in size compared to ~4 MB for Glide, making it preferable for projects focused on APK size.
// Coil — lazy image loading
imageView.load("https://example.com/image.jpg") {
crossfade(true)
placeholder(R.drawable.placeholder)
size(512, 512)
memoryCachePolicy(CachePolicy.ENABLED)
}
Kingfisher is an iOS library with support for Swift Concurrency, disk and memory caching, and prefetching for UICollectionView. SDWebImage is an older library with Objective-C roots but with Swift support. Both libraries integrate with UIImageView and automatically manage the loading lifecycle: they cancel requests when a cell is reused, load images only when the cell is visible, and free memory upon a memory warning notification.
Large lists of data are the main area of application for Lazy Loading in mobile apps. News feeds, product catalogs, chats, transaction history — any screen with a potentially infinite list requires pagination and deferred loading.
Paging 3 is a library from Android Jetpack that implements the full lazy loading cycle: data request from RemoteMediator (API + database), caching in Room, page-by-page output via PagingData, and display through AsyncPagingDataAdapter. Paging 3 supports three types of pagination: Page-based (pages), Item-based (offset/limit), and Key-based (pagination keys from API). Separator is built-in support for separators between pages for loading indicators.
// Paging 3 — lazy loading from 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 is a built-in SwiftUI container that creates and renders elements only when they appear on screen. Unlike VStack, which immediately computes the layout of all child elements, LazyVStack defers view creation until the element becomes visible or enters the prefetch range. LazyHStack is the horizontal counterpart for carousels. For large lists, Apple recommends using List, which internally works similarly to LazyVStack with added built-in recycling.
// SwiftUI — LazyVStack with lazy loading
ScrollView {
LazyVStack(spacing: 8) {
ForEach(articles) { article in
ArticleRow(article: article)
.onAppear {
if article == articles.last {
viewModel.loadMore()
}
}
}
}
}
Lazy loading of UI components is a technique where parts of the interface (headers, footers, settings sections, tabs) are created not at screen startup, but on first access. This speeds up initial render and reduces load on the main thread.
ViewStub is a lightweight View placeholder in Android that does not occupy space in the layout and does not create child Views until inflate() is called. It is ideal for rarely used sections: search panel, advanced settings, ad blocks. Fragment lazy loading is a technique where Fragment.onCreateView is deferred until the user switches to that tab. It is implemented via isVisible or UserVisibleHint in ViewPager.
AndroidX provides SplitInstallManager for on-demand lazy loading of modules. Setup, diagnostics, or additional feature modules are loaded as Dynamic Feature modules only on the user's first request. This reduces the base application size by 30–50% and simultaneously implements the principle of Lazy Loading not only at the data level but also at the code level.
TabView in SwiftUI loads each tab's content lazily — only when the tab is activated. UIKit UITabBarController creates all child controllers at startup by default, but this behavior can be changed by not adding them to tabBarController.viewControllers immediately and substituting them as the user switches. UIStackView with dynamically added arrangedSubviews also follows the Lazy Loading principle — add a Subview only when the user performs an action requiring that part of the interface.
For optimizing overall screen loading, combine Lazy Loading at all levels: ViewStub for rarely used sections, Paging 3 for data, Glide/Coil for images, and lazy ViewModel initialization through Hilt/Dagger Scopes or Swinject. This approach yields a screen that loads in 200–400 ms even on budget devices with 3 GB of RAM.
Frequently Asked Questions
If the screen is guaranteed to show few items (up to 20) and all of them are needed immediately, Lazy Loading is overkill. For lists that are rarely scrolled, Eager Loading can be simpler and faster to implement without noticeable performance loss.
It reduces peak memory consumption by 3–10 times for large lists, since only visible elements plus the prefetch buffer are stored in memory. However, adding prefetch and image caching creates a moderate memory overhead that needs to be managed.
List is preferable for homogeneous data with swipe, drag-and-drop, and built-in selection support. LazyVStack is for custom layouts with different cell types, sections, and non-standard spacing. List internally works like LazyVStack with additional functionality.
On Android, use the Layout Inspector to view the View hierarchy: with Lazy Loading, most elements should be absent from the tree. On iOS, use Xcode Debug View Hierarchy. If all elements are present during scrolling, Lazy Loading is not working.
Both parameters are important, but priority depends on the platform. On iOS with ARC and efficient memory management, loading speed takes priority. On Android with JVM and GC, memory savings take priority because every allocation on the main thread can cause a GC freeze.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also