Оптимизация загрузки изображений в мобильном приложении
Загрузка изображений — одна из тех задач, которую легко сделать «работающей» и очень сложно сделать правильной. Приложение грузит фото, показывает их — всё нормально. До момента, когда пользователь открывает галерею из 200 позиций, и Bitmap allocation в Android Profiler начинает рисовать горку: 4, 8, 14, 23 MB... Потом OOM. Или — более мягкий сценарий — пользователи на iPhone 12 в режиме Low Data Mode ждут 4–6 секунд до появления первого изображения, потому что загружается оригинал 4K вместо превью.
Ключевые проблемы
Неправильный размер изображения. Сервер отдаёт оригинал 2400×3200, ImageView — 80×80 dp. Glide / Kingfisher / Coil делают downsampling, но сначала эти 29 MB приходят по сети и декодируются в памяти. На Android BitmapFactory.Options.inSampleSize или параметры URL ресайза (?w=160&h=160&fit=crop) решают проблему до загрузки.
Отсутствие disk-кэша. По умолчанию SDWebImage кэширует на диск, но если кто-то установил SDWebImageOptions.refreshCached для «свежести» данных — каждый запуск приложения загружает все изображения заново. На экранах с аватарами пользователей это означает 20–30 лишних сетевых запросов при каждом открытии.
Последовательная загрузка вместо параллельной. Кастомные реализации через URLSession.dataTask нередко создают очередь, где следующий запрос стартует после завершения предыдущего. В списке из 10 элементов — ждём суммарно все 10 RTT вместо максимального из них.
Как решаем
На iOS стек по умолчанию: Kingfisher для Swift-проектов, SDWebImage для Obj-C legacy или проектов с CocoaPods-зависимостями. Kingfisher удобен KFImage в SwiftUI и нативной поддержкой @MainActor. Ключевые настройки:
KingfisherManager.shared.cache.diskStorage.config.sizeLimit = 500 * 1024 * 1024 // 500 MB
KingfisherManager.shared.cache.memoryStorage.config.totalCostLimit = 100 * 1024 * 1024 // 100 MB
Для прогрессивной загрузки JPEG — ImageDataProcessor с ProgressiveJPEGAddon. Пользователь видит размытое изображение сразу, а не плейсхолдер 2 секунды.
На Android: Coil для Compose-проектов (нативная AsyncImage), Glide для View-based. Glide умеет thumbnail(0.1f) — загружает 10% от оригинального размера как placeholder пока грузится полная версия. Для WebP-конвертации на сервере Glide справляется из коробки, Coil требует SvgDecoder / VideoFrameDecoder через отдельные зависимости.
Обязательный паттерн для обоих платформ: параметризованные URL с размером под конкретный ImageView. Если бэкенд на Cloudinary или imgproxy — передаём ?width={viewWidthDp * density}&format=webp&quality=80. WebP на 25–35% меньше JPEG при том же визуальном качестве для фотографий.
Placeholder стратегия
Пустой серый прямоугольник — плохо. BlurHash или ThumbHash — хорошо. Это компактный (20–30 байт) хэш изображения, который рендерится локально в виде цветного размытого превью до загрузки реального контента. На iOS — библиотека BlurHash, на Android — io.github.nicklockwood:thumbhash. Данные хэша приходят вместе с JSON-ответом API — нулевые сетевые затраты на превью.
Кейс: carousel с 50 изображениями
Клиент реализовал UIScrollView с UIImageView через page control. При открытии экрана — сразу загружал все 50 изображений. На медленном 3G WKWebView (да, был такой гибрид) закрывался по OOM через 3–4 листания.
Решение: lazy loading через UIPageViewController с окном видимости ±2 страницы. NSCache с лимитом 20 MB для decoded images. Остальные — только URL в памяти. Время до первого взаимодействия сократилось с 8 до 1.2 секунды.
Сроки
Аудит загрузки изображений и настройка библиотеки — 1–3 дня. Если нужна интеграция CDN-ресайза и BlurHash по всему приложению — 1 неделя.







