Оптимізація завантаження зображень у мобільній програмі
Завантаження зображень — одна з тих задач, яку легко зробити «робочою» та дуже складно зробити правильною. Програма грузит фото, показує їх — все нормально. До моменту, коли користувач відкриває галерею з 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 тиждень.







