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 и автоматически управляют жизненным циклом загрузки: отменяют запросы при переиспользовании ячейки, загружают изображения только когда ячейка видна и освобождают память при notification о нехватке памяти.
Большие списки данных — основная сфера применения 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 с добавлением built-in 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 предпочтительнее для однородных данных с возможностью свайпа, перетаскивания и built-in selection. 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 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также