mcpField
P
performance-optimization

performance-optimization

github.com/kaakati開発1036 回インストール39 週間前に更新
リポジトリ

performance-optimization について

performance-optimizationは、SwiftおよびSwiftUIアプリの開発者向けの性能判断ガイドです。変更前のプロファイリングを重視し、画面更新、メモリ管理、画像読み込み、バックグラウンド処理のパターンを紹介します。

主な機能

  1. 01プロファイリングツール選択の指針
  2. 02SwiftUIビュー更新の戦略
  3. 03メモリリークとキャッシュの対策
  4. 04画像ダウンサンプリングのパターン
  5. 05バックグラウンド処理の推奨事項

活用シーン

  1. 01画面のカクつきやフレーム落ちの調査
  2. 02メモリ増加や循環参照の追跡
  3. 03SwiftUIビューの反復計算の削減
  4. 04メモリ制約下でのサムネイル読み込み

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/kaakati/rails-enterprise-dev --skill performance-optimization

インストール後、Claude Code で/performance-optimizationを入力して読み込まれたか確認します。

同梱ファイルreferences/complete-performance-toolkit.md

詳細

パフォーマンス最適化 — エキスパートの判断

パフォーマンスに関する選択のための、エキスパート向け判断フレームワークです。Claude は遅延読み込みや非同期処理の基本を把握しています。このスキルでは、最適化すべきタイミングと使用するツールを判断します。


判断ツリー

最適化すべきですか?
When should you invest in optimization?
├─ User-facing latency issue (visible stutter/delay)
│  └─ YES — Profile and fix
│     Measure first, optimize second
│
├─ Premature concern ("this might be slow")
│  └─ NO — Wait for evidence
│     Write clean code, profile later
│
├─ Battery drain complaints
│  └─ YES — Use Energy Diagnostics
│     Focus on background work, location, network
│
├─ Memory warnings / crashes
│  └─ YES — Use Allocations + Leaks
│     Find retain cycles, unbounded caches
│
└─ App store reviews mention slowness
   └─ YES — Profile real scenarios
      User perception matters

落とし穴:思い込みに基づいて最適化することです。まず必ずプロファイリングしてください。ボトルネックは、考えている場所とは異なることがほとんどです。

プロファイリングツールの選択
What are you measuring?
├─ Slow UI / frame drops
│  └─ Time Profiler + View Debugger
│     Find expensive work on main thread
│
├─ Memory growth / leaks
│  └─ Allocations + Leaks instruments
│     Track object lifetimes, find cycles
│
├─ Network performance
│  └─ Network instrument + Charles/Proxyman
│     Latency, payload size, request count
│
├─ Disk I/O issues
│  └─ File Activity instrument
│     Excessive reads/writes
│
├─ Battery drain
│  └─ Energy Log instrument
│     CPU wake, location, networking
│
└─ GPU / rendering
   └─ Core Animation instrument
      Offscreen rendering, overdraw
SwiftUI のビュー更新戦略
View is re-rendering too often?
├─ Caused by parent state changes
│  └─ Extract to separate view
│     Child doesn't depend on changing state
│
├─ Complex computed body
│  └─ Cache expensive computations
│     Use ViewModel or memoization
│
├─ List items all updating
│  └─ Check view identity
│     Use stable IDs, not indices
│
├─ Observable causing cascading updates
│  └─ Split into multiple @Published
│     Or use computed properties
│
└─ Animation causing constant redraws
   └─ Use drawingGroup() or limit scope
      Rasterize stable content
メモリ管理の判断
How to fix memory issues?
├─ Steady growth during use
│  └─ Check caches and collections
│     Add eviction, use NSCache
│
├─ Growth tied to navigation
│  └─ Check retain cycles
│     weak self in closures, delegates
│
├─ Large spikes on specific screens
│  └─ Downsample images
│     Load at display size, not full resolution
│
├─ Memory not released after screen dismissal
│  └─ Debug object lifecycle
│     deinit not called = retain cycle
│
└─ Background memory pressure
   └─ Respond to didReceiveMemoryWarning
      Clear caches, release non-essential data

絶対にしないこと

ビューの識別性

識別子としてインデックスを絶対に使用しないでください:

// ❌ Identity changes when array mutates
List(items.indices, id: \.self) { index in
    ItemRow(item: items[index])
}
// Insert at index 0 → all views recreated!

// ✅ Use stable identifiers
List(items) { item in
    ItemRow(item: item)
        .id(item.id)  // Stable across mutations
}

body 内で負荷の高い値を絶対に計算しないでください:

// ❌ Called on every render
var body: some View {
    let sortedItems = items.sorted { $0.date > $1.date }  // O(n log n) per render!
    let filtered = sortedItems.filter { $0.isActive }

    List(filtered) { item in
        ItemRow(item: item)
    }
}

// ✅ Compute in ViewModel or use computed property
@MainActor
class ViewModel: ObservableObject {
    @Published var items: [Item] = []

    var displayItems: [Item] {
        items.filter(\.isActive).sorted { $0.date > $1.date }
    }
}
状態管理

渡されたオブジェクトに @StateObject を絶対に使用しないでください:

// ❌ Creates new instance on every parent update
struct ChildView: View {
    @StateObject var viewModel: ChildViewModel  // Wrong!

    var body: some View { ... }
}

// ✅ Use @ObservedObject for passed objects
struct ChildView: View {
    @ObservedObject var viewModel: ChildViewModel  // Parent owns it

    var body: some View { ... }
}

すべてを @Published に絶対にしないでください:

// ❌ Every property change triggers view updates
class ViewModel: ObservableObject {
    @Published var items: [Item] = []
    @Published var internalCache: [String: Data] = [:]  // UI doesn't need this!
    @Published var isProcessing = false  // Maybe internal only
}

// ✅ Only publish what UI observes
class ViewModel: ObservableObject {
    @Published var items: [Item] = []
    @Published var isLoading = false

    private var internalCache: [String: Data] = [:]  // Not @Published
    private var isProcessing = false  // Private state
}
メモリリーク

エスケープクロージャで self を強参照絶対にしないでください:

// ❌ Retain cycle — never deallocates
class ViewModel {
    var timer: Timer?

    func start() {
        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
            self.tick()  // Strong capture!
        }
    }
}

// ✅ Weak capture + invalidation
class ViewModel {
    var timer: Timer?

    func start() {
        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
            self?.tick()
        }
    }

    deinit {
        timer?.invalidate()
    }
}

オブザーバーの削除を絶対に忘れないでください:

// ❌ Leaks observer and potentially self
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(handleNotification),
            name: .userLoggedIn,
            object: nil
        )
        // Never removed!
    }
}

// ✅ Remove in deinit or use modern API
class ViewController: UIViewController {
    private var observer: NSObjectProtocol?

    override func viewDidLoad() {
        super.viewDidLoad()
        observer = NotificationCenter.default.addObserver(
            forName: .userLoggedIn,
            object: nil,
            queue: .main
        ) { [weak self] _ in
            self?.handleNotification()
        }
    }

    deinit {
        if let observer { NotificationCenter.default.removeObserver(observer) }
    }
}
画像の読み込み

サムネイルにフル解像度の画像を絶対に読み込まないでください:

// ❌ 4000×3000 image for 80×80 thumbnail
let image = UIImage(contentsOfFile: path)  // Full resolution in memory!
imageView.image = image

// ✅ Downsample to display size
func downsampledImage(at url: URL, to size: CGSize) -> UIImage? {
    let options: [CFString: Any] = [
        kCGImageSourceShouldCache: false,
        kCGImageSourceCreateThumbnailFromImageAlways: true,
        kCGImageSourceThumbnailMaxPixelSize: max(size.width, size.height) * UIScreen.main.scale
    ]

    guard let source = CGImageSourceCreateWithURL(url as CFURL, nil),
          let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else {
        return nil
    }
    return UIImage(cgImage: cgImage)
}

上限を設けずに画像を絶対にキャッシュしないでください:

// ❌ Unbounded memory growth
class ImageLoader {
    private var cache: [URL: UIImage] = [:]  // Grows forever!

    func image(for url: URL) -> UIImage? {
        if let cached = cache[url] { return cached }
        let image = loadImage(url)
        cache[url] = image  // Never evicted
        return image
    }
}

// ✅ Use NSCache with limits
class ImageLoader {
    private let cache = NSCache<NSURL, UIImage>()

    init() {
        cache.countLimit = 100
        cache.totalCostLimit = 50 * 1024 * 1024  // 50 MB
    }

    func image(for url: URL) -> UIImage? {
        if let cached = cache.object(forKey: url as NSURL) { return cached }
        guard let image = loadImage(url) else { return nil }
        cache.setObject(image, forKey: url as NSURL, cost: image.jpegData(compressionQuality: 1)?.count ?? 0)
        return image
    }
}
負荷の高い処理

メインスレッドで負荷の高い処理を絶対に実行しないでください:

// ❌ UI frozen during processing
func loadData() {
    let data = try! Data(contentsOf: largeFileURL)  // Blocks main thread!
    let parsed = parseData(data)  // Still blocking!
    self.items = parsed
}

// ✅ Use background thread, update on main
func loadData() async {
    let items = await Task.detached(priority: .userInitiated) {
        let data = try! Data(contentsOf: largeFileURL)
        return parseData(data)
    }.value

    await MainActor.run {
        self.items = items
    }
}

必須パターン

効率的なリストビュー
struct EfficientListView: View {
    let items: [Item]

    var body: some View {
        ScrollView {
            LazyVStack(spacing: 12) {  // Lazy = on-demand creation
                ForEach(items) { item in
                    ItemRow(item: item)
                        .id(item.id)  // Stable identity
                }
            }
        }
    }
}

// Equatable row prevents unnecessary updates
struct ItemRow: View, Equatable {
    let item: Item

    var body: some View {
        HStack {
            AsyncImage(url: item.imageURL) { image in
                image.resizable().aspectRatio(contentMode: .fill)
            } placeholder: {
                Color.gray.opacity(0.3)
            }
            .frame(width: 60, height: 60)
            .clipShape(RoundedRectangle(cornerRadius: 8))

            VStack(alignment: .leading) {
                Text(item.title).font(.headline)
                Text(item.subtitle).font(.caption).foregroundColor(.secondary)
            }
        }
    }

    static func == (lhs: ItemRow, rhs: ItemRow) -> Bool {
        lhs.item.id == rhs.item.id &&
        lhs.item.title == rhs.item.title &&
        lhs.item.subtitle == rhs.item.subtitle
    }
}
メモリ安全な ViewModel
@MainActor
final class ViewModel: ObservableObject {
    @Published private(set) var items: [Item] = []
    @Published private(set) var isLoading = false

    private var cancellables = Set<AnyCancellable>()
    private var loadTask: Task<Void, Never>?

    func load() {
        loadTask?.cancel()  // Cancel previous

        loadTask = Task {
            guard !Task.isCancelled else { return }

            isLoading = true
            defer { isLoading = false }

            do {
                let items = try await API.fetchItems()
                guard !Task.isCancelled else { return }
                self.items = items
            } catch {
                // Handle error
            }
        }
    }

    deinit {
        loadTask?.cancel()
        cancellables.removeAll()
    }
}
デバウンス検索
@MainActor
final class SearchViewModel: ObservableObject {
    @Published var searchText = ""
    @Published private(set) var results: [Item] = []

    private var searchTask: Task<Void, Never>?

    init() {
        // Debounce search
        $searchText
            .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
            .removeDuplicates()
            .sink { [weak self] text in
                self?.performSearch(text)
            }
            .store(in: &cancellables)
    }

    private func performSearch(_ query: String) {
        searchTask?.cancel()

        guard !query.isEmpty else {
            results = []
            return
        }

        searchTask = Task {
            do {
                let results = try await API.search(query: query)
                guard !Task.isCancelled else { return }
                self.results = results
            } catch {
                // Handle error
            }
        }
    }
}

クイックリファレンス

Instruments の選択
問題インストゥルメント確認する内容
UI が遅いTime Profilerメインスレッドでの負荷の高い処理
メモリリークLeaksリークしたオブジェクト
メモリの増加Allocations増加しているカテゴリ
バッテリーEnergy Logウェイクの頻度
ネットワークNetworkリクエスト数、サイズ
ディスクFile Activity過剰な I/O
GPUCore Animationオフスクリーンレンダリング
SwiftUI パフォーマンスチェックリスト
問題解決策
リストのスクロールが遅いLazyVStack/LazyVGrid を使用する
すべての項目が再レンダリングされる安定した ID、Equatable の行を使用する
body の計算負荷が高いViewModel に移動する
@Published の更新が連鎖する分割するか、computed を使用する
アニメーションがカクつくdrawingGroup() を使用する
メモリ管理
パターン防止できる問題
クロージャ内の [weak self]参照サイクル
deinit での Timer.invalidate()タイマーリーク
deinit でオブザーバーを削除するオブザーバーリーク
上限付きの NSCacheキャッシュの無制限な増加
画像のダウンサンプリングメモリ使用量の急増
カスタムプロファイリングでの os_signpost
import os.signpost

let log = OSLog(subsystem: "com.app", category: .pointsOfInterest)

os_signpost(.begin, log: log, name: "DataProcessing")
// Expensive work
os_signpost(.end, log: log, name: "DataProcessing")
要注意の兆候
問題の兆候問題修正方法
List の ID にインデックスを使用変更時にビューが再作成される安定した識別子を使用する
body の計算負荷が高いレンダリングのたびに実行されるViewModel に移動する
渡されたオブジェクトに @StateObject を使用新しいインスタンスが作成される@ObservedObject を使用する
Timer/クロージャ内の self が強参照参照サイクル[weak self] を使用する
サムネイルにフル解像度の画像を使用メモリが急増する表示サイズにダウンサンプリングする
上限のない辞書キャッシュメモリが増加する上限付きの NSCache を使用する
Task.detached を使わずに負荷の高い処理を実行メインスレッドをブロックするバックグラウンド優先度を使用する

コメント