← Назад к вопросам

Как дебажить тормозящий скролл?

2.0 Middle🔥 251 комментариев
#UIKit и верстка#Тестирование и отладка

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Отладка проблем с производительностью скролла в iOS

Когда сталкиваешься с тормозящим скроллом (janky, laggy scrolling), важно подходить к проблеме системно. В iOS, особенно в UIKit (UICollectionView/UITableView) и SwiftUI, причины часто схожи, но инструменты отладки могут отличаться.

Инструменты профилирования

  1. Instruments – Core Animation

    • Запускаете через ProductProfile или 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
    
  2. Instruments – Time Profiler

    • Показывает, какой код выполняется в main thread. Скролл должен быть максимально лёгким для главного потока.
    • Фильтруйте по своему модулю, смотрите тяжёлые методы (например, синхронные сетевые запросы, сложные вычисления).
  3. Инструменты 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-специфика

  1. Использование List вместо ScrollView + VStack для динамических данных – List автоматически переиспользует ячейки.
  2. Идентификаторы (id: .self) для структур данных могут вызвать лишние перерисовки. Используйте стабильные Identifiable.
  3. Модификаторы drawingGroup(), compositingGroup() могут помочь с оффскрин-рендерингом, но иногда и ухудшают.
  4. Инструмент Xcode View Debugger для SwiftUI также показывает предупреждения о производительности.

Практическая стратегия отладки

  1. Изолируйте проблему: Упростите ячейку до минимума (например, оставив только UILabel). Если скролл плавный – добавляйте элементы по одному, чтобы найти виновника.
  2. Замеряйте FPS в релизной сборке (ProductArchiveDistribute AppDevelopmentProfile), так как debug-сборка может скрывать проблемы.
  3. Используйте CADisplayLink для кастомного замера FPS в реальном времени в приложении.
  4. Проверьте фоновые активности: Обновление локации, таймеры, сетевые коллбеки могут непреднамеренно нагружать main thread.

Помните, что плавный скролл – это perception. Иногда достаточно сделать анимацию загрузки контента более плавной или использовать willDisplayCell для отложенной настройки сложных элементов, чтобы пользователь воспринимал интерфейс как отзывчивый, даже если данные подгружаются с небольшой задержкой.

Как дебажить тормозящий скролл? | PrepBro