🚅

SwiftUIにおける@State(struct)とObservableのパフォーマンスをInstruments比較

に公開

@State(struct), Observableのパフォーマンス比較

iOS 26でのパフォーマンスをXcode 26のInstrumentsのSwiftUI Profileを使って比較します。

ここでの@Stateはstructに付与された物に言及しておりObservable(class)に付与しているものを指していません。

// 例
@State var selectedId: Int?
@State var text: String = ""
@State var item: Item

シンプルなListで並べられたItemを追加、変更するようなViewを作成します。
選択されたセルは背景が赤色になるようにします。

  • 追加(Creation):右上の追加ボタン
  • 変更(Update):選択状態

コード

コード
import SwiftUI
import Combine

struct Cell: View {
  var value: Int
  var selected: Bool
  
  var body: some View {
    Text("Value: \(value)")
      .frame(maxWidth: .infinity, alignment: .leading)
      .listRowBackground(selected ? Color.red : nil)
  }
}

struct StateListView: View {
  @State var items = Array(0..<10)
  @State var selectedItem: Int = 0
  
  var body: some View {
    NavigationStack {
      ScrollView {
        LazyVStack {
          ForEach(items, id: \.self) { item in
            Cell(value: item, selected: selectedItem == item)
              .contentShape(.rect)
              .onTapGesture {
                selectedItem = item
              }
          }
        }
      }
      .toolbar {
        Button {
          items.append(items.max(by: { $0 < $1 })! + 1)
        } label: {
          Image(systemName: "plus")
        }
      }
    }
  }
}

struct ObservableListView: View {
  @Observable
  final class State {
    var items = Array(0..<10)
    var selectedItems: Int = 0
  }
  
  var state = State()
  
  var body: some View {
    NavigationStack {
      ScrollView {
        LazyVStack {
          ForEach(state.items, id: \.self) { item in
            Cell(value: item, selected: state.selectedItems == item)
              .contentShape(.rect)
              .onTapGesture {
                state.selectedItems = item
              }
          }
        }
      }
      .toolbar {
        Button {
          state.items.append(state.items.max(by: { $0 < $1 })! + 1)
        } label: {
          Image(systemName: "plus")
        }
      }
    }
  }
}

struct ObservableObjectListView: View {
  final class State: ObservableObject {
    @Published var items = Array(0..<10)
    @Published var selectedItem: Int = 0
  }
  
  @StateObject var state = State()
  
  var body: some View {
    NavigationStack {
      List {
        ForEach(state.items, id: \.self) { item in
          Cell(value: item, selected: state.selectedItem == item)
            .contentShape(.rect)
            .onTapGesture {
              state.selectedItem = item
            }
        }
      }
      .toolbar {
        Button {
          state.items.append(state.items.max(by: { $0 < $1 })! + 1)
        } label: {
          Image(systemName: "plus")
        }
      }
    }
  }
}

Profileの結果

操作手順

  1. Cellの1, 2, 3を順にタップして選択されたCellを変更
  2. Addボタンを3回タップ

理想

  • List(Creation)は1回のみ
  • List(Update)はセルを3回追加(Create)しているため3回
  • Cell(Creation)は初回の10回作成と追加の3回のため13回
  • Cell(Update)は選択したセルを3回選んで変更しているため、Onになった3回、Offになった3回の合計6回

結果

Name List(Creation) List(Update) Cell(Creation) Cell(Update)
@State 1 6 13 6
ObservableObject 1 6 13 6
Observable 1 3 13 6
理想 1 3 13 6

考察

List(Update)の結果が異なります。
Observableに比べて、@StateObservableObjectは3回多いです。
本来はCellがCreate(追加)された3回のみListがUpdateされるべきですが、@State、ObservableObjectはCellのCreation時だけでなくCellのUpdate時にもListがUpdateされてしまっています。

下図の@State StateListView.selectedItemの3回が余計

結論

@StateではなくObservableで状態管理すると再描画パフォーマンスが良くなります。
これはListやScrollViewの違いに依存しません。

パフォーマンスの差が出る理由は@Stateはstructの変更のみ監視でき、structは変更があると全て再構築されてしまいますが、Observableはclassのため必要な部分のみ更新可能だからだと思います。

参考文献

https://developer.apple.com/videos/play/wwdc2025/306/

Discussion