SwiftUI позиционирует себя как декларативный фреймворк, где вы просто описываете, что должно быть на экране, а система сама разбирается с обновлениями. Это удобно ровно до тех пор, пока приложение не начинает тормозить без видимых причин. Чаще всего проблема не в кривых руках, а в том, как именно вы строите дерево вьюх. Разбираемся, где SwiftUI теряет кадры и как этого избежать.
Как SwiftUI на самом деле обновляет экран:
Когда в вашей View меняется
@State или другое отслеживаемое свойство, SwiftUI перезапускает вычисление body и сравнивает результат с предыдущим. Только те части интерфейса, где обнаружились различия, отправляются на рендеринг. Остальные игнорируются.Этот механизм (диффинг) работает быстро, но у него есть цена: чем больше примитивных вьюх в body, тем больше работы алгоритму. И если вы написали одну гигантскую структуру на 200 строк, каждое изменение состояния заставит SwiftUI пройти по всей иерархии и сравнить каждый Text, каждый Image и каждую кнопку.
Главный враг - монолитное body:
Многие разработчики, пытаясь навести порядок, выносят части интерфейса в отдельные функции или вычисляемые свойства. Это делает код чище для глаз, но для SwiftUI ничего не меняет. Во время компиляции все эти вызовы схлопываются обратно в единое дерево примитивов. Производительность остается такой же, как если бы все было написано в одну строку.
Проверить это легко: добавьте рандомный фон к вьюхам в таких функциях. При каждом обновлении цвета будут меняться - значит, body пересчитывается целиком, несмотря на визуальное разделение.
Верный способ - отдельные структуры:
Единственный способ создать настоящие границы перерисовки - выносить части интерфейса в отдельные структуры, реализующие протокол View. Когда SwiftUI видит вложенную структуру, он сначала проверяет, изменились ли ее входные параметры. Если нет - body этой структуры даже не вызывается. Диффинг останавливается на этом уровне.
struct Counter: View {
@State private var value = 0
var body: some View {
VStack {
Button("Увеличить: \(value)") { value += 1 }
SubView()
}
}
}
struct SubView: View {
var body: some View {
Text("Данная структура не зависит от value")
}
}
В этом примере нажатие на кнопку перезапустит body у Counter, но SubView останется нетронутым. SwiftUI видит, что его параметры не изменились, и пропускает вычисления.
Коварство замыканий и Equatable:
Есть один случай, когда даже отдельные структуры могут перерисовываться без необходимости: если в них передаются замыкания. SwiftUI не умеет сравнивать замыкания по содержимому, поэтому может считать, что каждое новое замыкание - это изменение.
В таких ситуациях помогает явное указание, что вьюха поддерживает Equatable. Добавьте модификатор .equatable() и реализуйте метод ==, где явно опишите, какие поля должны влиять на перерисовку. Обратите внимание: просто подписать структуру под Equatable недостаточно, нужно еще сказать SwiftUI использовать эту информацию через .equatable().
Как отлавливать лишние перерисовки:
Самый простой способ проверить, пересоздается ли вьюха без необходимости: добавить случайный фон. Если цвета прыгают при каждом действии, значит, body вызывается чаще, чем нужно. Это хороший маркер для рефакторинга.
🔗 Ссылка на подробную статью
💡 Вывод:
SwiftUI дает мощные инструменты для оптимизации, но они работают только если вы понимаете, как устроен движок рендеринга. Главное правило: чем меньше примитивных вьюх проходит через диффинг при каждом обновлении, тем лучше. Выносите независимые части интерфейса в отдельные структуры, следите за замыканиями и не доверяйте функциям и вычисляемым свойствам - они только маскируют проблему. Архитектура вьюх напрямую влияет на производительность, и игнорировать это нельзя.
➡️ Подписаться на канал
Мобильный трудоголик