Як ми прискорили запуск iOS застосунку на 30% — Experiments Hub

💡 Усі статті, обговорення, новини про Mobile — в одному місці. Приєднуйтесь до Mobile спільноти!

Привіт, спільното!
Я Владислав Вдовиченко, iOS Tech Lead у компанії zakaz.ua. У цій статті хочеться поділитись досвідом того, як вдалось прискорити мережеві запити у нашому iOS застосунку. Це скоротило час завантаження домашньої сторінки на 30%. Основним діагностичним інструментом є Xcode Instruments (Network).

Яка ціль усього цього?

Проблема: при «холодному» запуску iOS застосунку домашня сторінка може завантажуватись кілька секунд. Необхідно знайти шляхи для оптимізації та впровадити їх.

Аналіз

Як вже згадано вище, аналіз виконувався вбудованим в Xcode інструментом — Instruments (Network) (вибачте за тавтологію, ¯\_(ツ)_/¯). Процитую документацію Apple:

Інструмент внутрішньо збирає детальну інформацію — включно з заголовками та вмістом тіла, тривалістю встановлення з’єднання, а також часом запиту і відповіді — з системи завантаження URL. Потім він записує ці дані й об’єднує їх у точні інтервали з додатковими метаданими.

Всередині нас зустріне ось таке вікно:

Запис сесії або «прогонка» відбувається на реальному пристрої із заздалегідь обраним застосунком, який аналізується. Обрати можна лише власні застосунки, до інших дебаггер не підключиться. Дозволяється записувати «прогонки» багато разів. Це корисно для порівняння показників і зменшення похибки, знадобиться пізніше.

Результати «прогонки»

Які проміжні висновки можна зробити?

  • При старті виконується велика кількість запитів одночасно (GET і POST до різних ендпоінтів API).
  • Багато запитів працюють паралельно, але кожен має свою тривалість (від ~150 до ~900 мс). Тобто ситуацій коли запит x чекає на відповідь запиту y майже немає.
  • Запити виконуються в різних URLSession.

Оскільки проблем з організацією флоу усього завантаження у нашому випадку немає, то пропоную детальніше розглянути роботу URLSession. В протилежному випадку я б пропонував працювати над усуненням залежностей між запитами.

Робота з URLSession

Технічний опис

У документації до URLSessionTaskTransactionMetrics є наочна схема, яка добре пояснює послідовність виконання мережевого запиту.

Знімок екрана 2025-07-03 о 21.39.48.png

  1. Спочатку створюється Task для запиту.
  2. Потім відбувається перетворення доменного імені через DNS у потрібну IP-адресу, куди треба підключитися.
  3. Далі встановлюється TCP-зʼєднання, після чого налаштовується захищене TLS-зʼєднання.
  4. І вже наприкінці надсилаються дані на сервер, очікується відповідь і отримується результат.

Якщо запитів декілька, схема ще більше відрізняється: наприклад, IP-адреса стає відомою вже після першого запиту, і для наступних не потрібно щоразу встановлювати нове зʼєднання — залишається лише етап обміну даними.

Поточна реалізація

В нашому iOS застосунку використовуються Service-класи, що відповідають за роботу з мережею для кожного окремого сегменту бізнес-логіки (наприклад, CartService, UserService тощо). Кожен сервіс має власний екземпляр URLSession. При першому запиті URLSession встановлює TCP- та TLS-з’єднання (особливо при використанні HTTP/2).

Через це кожен сервіс при першому запиті налаштовує окреме безпечне підключення, що додає від 50 до 200 мс затримки на кожне з’єднання. У результаті стартові запити домашньої сторінки можуть значно сповільнювати завантаження.

image-20250703-084551.png

Що можна побачити на схемі:

  • Фіолетовий сегмент показує період, коли запит фактично призупинений через очікування підключення до хоста і ще не виконується. Якщо використовується HTTP/2, одне підключення до сервера повторно використовується всіма запитами в межах однієї URLSession.
  • Сині відрізки позначають операції встановлення зʼєднання, через які затримуються інші запити. Після першого підключення у наступних запитах такого блокування вже немає, оскільки вони використовують існуюче зʼєднання. Всередині підключення є три основні кроки:
    • DNS-резолюція — перетворення доменного імені на IP-адресу.
    • TCP-підключення — встановлення каналу звʼязку з сервером.
    • TLS — налаштування захищеного шифрованого зʼєднання.
  • Після успішного встановлення підключення обидва запити, які чекали, починають виконуватись.
  • Сіра зона відповідає часу очікування відповіді від сервера.
  • Далі видно короткий зелений сегмент, що відображає отримання даних. Його ширина залежить від обсягу відповіді.
  • Зеленим кольором позначені успішні запити, а помаранчевим можуть позначатись ті, що завершилися помилкою.

Як це вирішити?

Щоб усунути зайві витрати часу, вирішено використовувати спільний екземпляр URLSession для всіх Service-класів. Якщо URLSession уже встановила безпечне з’єднання з API, повторне встановлення не потрібне. Це не впливає на підтримку паралельних запитів і не знижує продуктивність.

Знімок екрана 2025-06-27 о 11.56.47-20250627-085652.png

Безпечне зʼєднання встановлюється в найпершому запиті (у даному випадку це user/profile). Наступні запити вже не мають потреби встановлювати безпечне зʼєднання.

Також варто зазначити, що цілком допускається використання різних URLSession для різних хостів (основні запити, аналітика, картинки, логи тощо). Це може бути потрібно коли хости потребують різної конфігурації.

Трохи чаклування та магії

Щоб ще більше покращити результат, одразу після ініціалізації всіх Service-класів та інʼєкції URLSession відправляється «прогрівочний» запит до кореня API. Це робиться максимально рано, ще до запуску основної логіки та «робочих» запитів. Таким чином, безпечне з’єднання вже готове до моменту, коли починають надходити реальні запити.

extension URLSession {
    func preheatAPI() {
        guard let url = URL(string: "https://stores-api.zakaz.ua/") else { return }

        let request = URLRequest(url: url)
        let task = self.dataTask(with: request) { _, _, _ in }
        task.resume()
    }
}

Виклик цього методу відбувається одразу після ініціалізації URLSession.

На зображенні вище «прогрівочний» запит встановлює безпечне встановлює TCP- та TLS-з’єднання що прискорює роботу подальших запитів.

Заміри результатів

Після проведених робіт постало завдання зробити заміри. Необхідно зʼясувати чи внесені покращення дають хоч якийсь ефект та чи сильно він помітний. Прийнято рішення використати Point of Interest для розташування «маркерів».

Point of Interest (POI) у Xcode Instruments — це спеціальна мітка, яку можна вставити у свій код, щоб виділити важливий момент під час профілювання.

Для об’єктивного вимірювання швидкості завантаження домашньої сторінки було проведено експеримент:

  • Додано маркер початку завантаження екрану (метод viewDidLoad()) та старту всіх необхідних запитів.
  • Додано маркер завершення завантаження екрану, коли весь контент відмальовано.
  • Виконано 5 запусків для обчислення середніх значень.

Результати вимірів:




До оптимізації (сек)



Після оптимізації (сек)



1


1.90


1.35


2


2.03


1.37


3


2.18


1.44


4


1.99


1.50


5


1.95


1.40


Середнє


2.01


1.41

Screenshot 2025-07-03 at 15.09.52.png

Висновки

Впровадивши перевикористання спільної URLSession кожен окремий початковий запит став виконуватись на 30–50% швидше, а завантаження домашньої сторінки прискорилося приблизно на 30% (або ±600 ms).

👍ПодобаєтьсяСподобалось4
До обраногоВ обраному0
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Підписатись на коментарі