← Назад к вопросам
Как дебажить тормозящий скролл?
2.0 Middle🔥 251 комментариев
#UIKit и верстка#Тестирование и отладка
Комментарии (1)
🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Отладка проблем с производительностью скролла в iOS
Когда сталкиваешься с тормозящим скроллом (janky, laggy scrolling), важно подходить к проблеме системно. В iOS, особенно в UIKit (UICollectionView/UITableView) и SwiftUI, причины часто схожи, но инструменты отладки могут отличаться.
Инструменты профилирования
-
Instruments – Core Animation
- Запускаете через
Product→Profileили Cmd+I. - Core Animation FPS: Показывает количество кадров в секунду. Цель – стабильные 60 FPS (или 120 на ProMotion).
- Color Blended Layers: Подсвечивает овердроу (overdraw). Красные области – несколько слоёв, смешанных альфа-каналом. Зелёные – оптимально отрисованные.
// В коде можно включить визуализацию #if DEBUG if let window = UIApplication.shared.windows.first { window.layer.speed = 0.1 // Замедление анимаций для отладки } #endif - Запускаете через
-
Instruments – Time Profiler
- Показывает, какой код выполняется в main thread. Скролл должен быть максимально лёгким для главного потока.
- Фильтруйте по своему модулю, смотрите тяжёлые методы (например, синхронные сетевые запросы, сложные вычисления).
-
Инструменты Xcode
- Debug Navigator (во время запуска приложения): Следите за CPU, Memory, Energy.
- Debug View Hierarchy: Визуально анализируйте сложность иерархии вьюх.
Типичные причины и решения
1. Сложная иерархия вьюх и отсутствие переиспользования
- UITableView/UICollectionView без
dequeueReusableCellсоздают новые вьюхи каждый раз – это убийственно для производительности.
// ✅ Правильно
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! CustomCell
cell.configure(with: data[indexPath.row])
return cell
}
// ❌ Очень плохо
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = CustomCell() // Новая ячейка каждый раз!
return cell
}
2. Блокировка main thread
- Синхронные операции в
cellForRowAt(загрузка изображений, вычисления). - Решение: асинхронная загрузка + кэширование.
// Пример с асинхронной загрузкой изображения
func configure(with url: URL) {
DispatchQueue.global(qos: .userInitiated).async {
if let data = try? Data(contentsOf: url), let image = UIImage(data: data) {
DispatchQueue.main.async {
self.imageView.image = image // Обновление только на главном потоке
}
}
}
}
3. Неоптимальная работа с изображениями
- Загрузка больших изображений без ресайза.
- Используйте библиотеки типа SDWebImage, Kingfisher, которые делают:
- Асинхронную загрузку
- **Downsampling** (изменение размера под ImageView)
- Кэширование в памяти и на диске
// Kingfisher пример с оптимизацией
let processor = DownsamplingImageProcessor(size: imageView.bounds.size)
imageView.kf.setImage(with: url, options: [.processor(processor), .cacheOriginalImage])
4. Дорогая логика в layoutSubviews() и draw(_ rect:)
- Избегайте сложных вычислений в этих методах. Кешируйте результаты, если они не меняются.
- Для кастомного рисования используйте CAShapeLayer вместо переопределения
draw(_ rect:), когда возможно.
5. Отсутствие Prefetching для данных
UITableViewDataSourcePrefetchingиUICollectionViewDataSourcePrefetchingпозволяют начать загрузку данных заранее.
extension ViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
// Начинаем загрузку данных для indexPaths, которые скоро понадобятся
for indexPath in indexPaths {
startLoadingData(for: dataItem(at: indexPath))
}
}
}
6. Чрезмерное использование cornerRadius, shadows, masks
layer.cornerRadius+layer.masksToBounds = trueвызывает offscreen rendering. Это особенно дорого при скролле.- Оптимизации:
- Используйте **пререндеренные изображения** с закруглениями.
- Для простых закруглений – `UIBezierPath` и `CAShapeLayer`.
```swift
// Менее затратная альтернатива cornerRadius + masksToBounds
let path = UIBezierPath(roundedRect: bounds, cornerRadius: 10)
let maskLayer = CAShapeLayer()
maskLayer.path = path.cgPath
view.layer.mask = maskLayer
```
SwiftUI-специфика
- Использование
ListвместоScrollView + VStackдля динамических данных –Listавтоматически переиспользует ячейки. - Идентификаторы (id: .self) для структур данных могут вызвать лишние перерисовки. Используйте стабильные
Identifiable. - Модификаторы
drawingGroup(),compositingGroup()могут помочь с оффскрин-рендерингом, но иногда и ухудшают. - Инструмент
Xcode View Debuggerдля SwiftUI также показывает предупреждения о производительности.
Практическая стратегия отладки
- Изолируйте проблему: Упростите ячейку до минимума (например, оставив только
UILabel). Если скролл плавный – добавляйте элементы по одному, чтобы найти виновника. - Замеряйте FPS в релизной сборке (
Product→Archive→Distribute App→Development→Profile), так как debug-сборка может скрывать проблемы. - Используйте
CADisplayLinkдля кастомного замера FPS в реальном времени в приложении. - Проверьте фоновые активности: Обновление локации, таймеры, сетевые коллбеки могут непреднамеренно нагружать main thread.
Помните, что плавный скролл – это perception. Иногда достаточно сделать анимацию загрузки контента более плавной или использовать willDisplayCell для отложенной настройки сложных элементов, чтобы пользователь воспринимал интерфейс как отзывчивый, даже если данные подгружаются с небольшой задержкой.