Проблемы с производительностью SwiftUI часто сводятся к двум вещам: представления либо перерисовываются слишком часто, либо их body выполняется слишком долго. Прежде чем что-то исправлять, нужно понять, какие именно представления виноваты и что вызывает их обновление. Xcode Instruments способен показать это довольно точно — если знать, где смотреть.
В этой статье я покажу, как находить перерисовки SwiftUI и измерять их стоимость в Xcode Instruments. Мы запишем трассировку с шаблоном SwiftUI, изучим дорожку SwiftUI и представления View Body Updates, чтобы увидеть, какое представление сколько раз обновляется, сколько занимает каждое обновление и что именно его вызвало. Если вы раньше пользовались Self._printChanges(), то Instruments — следующий шаг, когда нужно понять, сколько эти перерисовки реально стоят.
Записываем SwiftUI-шаблон
Начните с Product → Profile (⌘I) в Xcode и выберите шаблон SwiftUI. По умолчанию профилирование собирает приложение в конфигурации Release — именно это и нужно. Debug-сборки искажают измерения, потому что выполняют не тот код, который запускают ваши пользователи.
Шаблон объединяет SwiftUI instrument с Time Profiler, а также инструментами Hangs и Hitches. Это удобно, потому что можно проверить, совпадает ли работа SwiftUI по времени с зависаниями и рывками интерфейса.
Запишите одно конкретное взаимодействие, на котором проявляется проблема: например, открытие экрана или прокрутку списка. Короткую и целевую запись анализировать намного проще, чем длинный тур по всему приложению. Кроме того, чем длиннее запись, тем больше данных приходится обрабатывать Instruments.
В этой статье я использую RocketTrace Slow Lab. Это намеренно медленное SwiftUI-приложение, которое я применяю для тестирования RocketTrace. Одна из его нагрузок генерирует 600 изменений состояния примерно с частотой 60 Гц — отличный способ получить наглядные плохие сигналы в Instruments.
Читаем трек SwiftUI
После остановки записи Instruments показывает трек SwiftUI в верхней части временной шкалы.
Первая дорожка, Update Groups, показывает, когда SwiftUI выполняет работу. Согласно сессии Apple Optimize SwiftUI performance with Instruments, если при скачке загрузки CPU эта дорожка пустая, проблема, скорее всего, находится не в SwiftUI.
Ниже расположены три трека для длинных обновлений: Long View Body Updates, Long Representable Updates и Other Long Updates. Обновления отображаются оранжевым и красным в зависимости от вероятности того, что они вносят вклад в рывок или зависание.
После раскрытия трека SwiftUI вы увидите те же три категории в виде отдельных поддорожек: View Body Updates, Representable Updates и Other Updates. В примере из статьи дорожка Hitches заполнена красным и есть два зависания, поэтому имеет смысл выяснить, какая часть проблемы приходится именно на обновления body.
Сводка внизу показывает длинные обновления. В 27-секундной записи было 228 длинных обновлений общей длительностью 1,10 секунды. Это хорошая отправная точка, но туда попадают только те обновления, которые Instruments считает длинными. Представление, которое обновляется сотни раз, но каждый раз имеет дешёвый body, здесь может почти не выделяться.
Открываем иерархию View Body Updates
Выберите дорожку View Body Updates и выделите мышью участок временной шкалы, который хотите изучить. В нижней части окна появятся обновления body для выбранного диапазона.
Клавишами ⌘3 и ⌘4 можно переключаться между View Body Updates View Hierarchy и таблицей View Body Updates. Начнём с иерархии.
Иерархия показывает представления, у которых в выбранном диапазоне выполнялся body, вместе со временем, которое они заняли. В примере все обновления body суммарно заняли 24,53 мс. WorkloadCard занял 8,35 мс, а InvalidationWorkloadCard — 6,71 мс, поэтому именно они оказались двумя самыми дорогими представлениями в выбранном интервале.
Фильтры в нижней части окна здесь очень полезны. Нажмите Call Tree, чтобы открыть их. Доступны:
- Separate by Category
- Separate by Thread
- Hide System Libraries
- Hide Inlined Functions
- Hide Modifiers
- Hide Primitive Elements
- Flatten Recursion
- Invert Call Tree
В примере включены Separate by Thread, Hide System Libraries и Invert Call Tree. Скрытие системных библиотек оставляет только представления вашего приложения — именно с этого списка лучше начинать. Если иерархия всё ещё слишком шумная, можно дополнительно скрыть модификаторы и примитивные элементы.
Ищем стоимость перерисовок в таблице
Нажмите ⌘4, чтобы перейти к таблице View Body Updates. Здесь видно уже не сгруппированные суммы, а каждое отдельное обновление.
Каждая строка — одно обновление body, со временем начала, длительностью, модулем, названием представления, категорией, описанием, корневыми причинами и процессом. В примере InvalidationWorkloadCard.body и WorkloadCard.body появляются снова и снова с промежутками всего в несколько миллисекунд. При этом большинство обновлений занимает лишь около 40–50 мкс.
Вот главный урок этой таблицы: одно обновление может быть очень дешёвым, но body представлений выполняются на главном потоке, поэтому большое количество дешёвых обновлений всё равно накапливается. Понять это можно только если смотреть одновременно и на частоту обновлений, и на длительность каждого из них.
Колонка Root Causes показывает, что именно вызвало каждое обновление. В примере там указан @Observable WorkloadController.(Int). То есть изменилось Int-свойство наблюдаемого WorkloadController, и каждое представление, которое его читало, снова выполнило свой body. Если выбрать строку, инспектор справа покажет время начала, длительность и иерархию представлений этого обновления.
Отслеживаем перерисовку до её причины
Root Causes даёт быстрый ответ, но иногда нужен дополнительный контекст. В документации Apple Understanding and improving SwiftUI performance объясняется, как открыть граф причин и следствий для группы обновлений. Задайте диапазон инспекции для группы, наведите указатель на обновление, нажмите появившуюся стрелку и выберите Show Causes. Синие узлы относятся к объектам вашего собственного кода, серые определены системой.
Сравните то, что показывает граф, с тем, что вы ожидаете от интерфейса. Apple выделяет две типичные причины слишком частых обновлений:
- Представление наблюдает за объектом с другими свойствами и обновляется при изменении одного из них. Apple рекомендует переходить на макрос
@Observable, который отслеживает только те свойства, которые действительно читает представление. - Обновление одного представления заставляет обновляться вложенные представления, хотя в UI ничего значимого не изменилось.
Если проблема не в частоте, а в дорогих обновлениях, решение обычно заключается в переносе тяжёлой работы из body. Выполняйте её при изменении данных, а представлению оставьте только отображение уже готового результата.
После каждого изменения записывайте то же самое взаимодействие заново. Только так можно подтвердить, что число обновлений или их длительность действительно уменьшились. Если вы хотите узнать больше о другой части этого рабочего процесса, ознакомьтесь с моей статьей об инструменте Time Profiler в Xcode Instruments.
Почему я перестал делать это вручную
Посмотрите на все шаги выше. Нужно записать трассировку, раскрыть дорожку, выбрать диапазон, переключаться между представлениями через ⌘3 и ⌘4, включать фильтры, сортировать таблицу и разбирать корневые причины обновлений по одной. Потом изменить код и повторить всё снова, чтобы проверить результат.
Мне оказалось очень сложно делать это вручную, и это занимает много времени. Нужно понимать, что искать, а нужная информация разбросана по нескольким трекам и представлениям. Именно поэтому я создал RocketTrace.
Автоматизируем это с RocketTrace
RocketTrace — приложение для Mac, которое анализирует записи Xcode Instruments. Можно либо записать сборку приложения прямо из RocketTrace, либо импортировать существующую трассировку. Затем приложение ранжирует найденные проблемы по измеренному влиянию. Весь анализ выполняется локально на Mac.
Вместо ручного просмотра дорожек и таблиц RocketTrace проверяет запись на семь типичных паттернов SwiftUI:
- широкое распространение инвалидизации, когда одна зависимость заставляет обновиться множество представлений;
- частые изменения состояния, когда одно свойство меняется настолько часто, что вызывает постоянные перерисовки;
- дорогие обновления
bodyпредставлений, вместе с их длительностью и backtrace; - перерисовки без зарегистрированного изменения значения;
- представления, которые обновляются с постоянной частотой;
- самое дорогое представление;
- повторяющиеся предупреждения времени выполнения.
Каждая найденная проблема содержит измеренные данные из трассировки, поэтому сразу видно, какое представление обновляется чаще всего или занимает больше всего времени. При этом решать, является ли конкретный паттерн ошибкой или ожидаемым поведением UI, всё равно приходится вам.
Из найденной проблемы можно скопировать промпт для кодинг-агента или позволить агенту выполнить весь процесс целиком.
Заключение
Поиск перерисовок SwiftUI в Xcode Instruments сводится к нескольким шагам: записать трассировку с шаблоном SwiftUI, изучить дорожку SwiftUI, проверить иерархию и таблицу View Body Updates и проследить корневую причину до зависимости, вызывающей обновления. Это работает, но вручную занимает довольно много времени.
RocketTrace можно скачать бесплатно, а первые три анализа с найденными проблемами включены. Запишите экран, производительность которого вызывает у вас подозрения, и посмотрите, что обнаружит инструмент. Если хотите ещё глубже разобраться в SwiftUI, загляните в раздел SwiftUI. Если у вас есть дополнительные советы или обратная связь, можно связаться с автором в Twitter.
