【iOS】もしかしてRealmのせいでビルド遅い？

iOS 開発でローカルでのデータ永続化を行うための選択肢として、`UserDefaults`や`Realm`がよく挙げられると思います。
`Realm`は高性能なローカルストレージで売り出されているので、高速での読み書きを行いたい場合などはよく使われていると思っています。
ただ、WWDC23 で`SwiftData`という Apple 純正の高機能ローカルストレージライブラリが登場したことで、高性能なローカルストレージの新たな選択肢として、`SwiftData`も挙がるようになりました。
今まで私は、業務ではもちろん、個人開発する時も`Realm`をよく使っていましたが、`SwiftData`もいつか使ってみたいなーとか思っていました。

そんな中あるいつものように Realm を使っていると、ちょっとした違和感を感じました。
みなさんも、Realm を使っていてこんな経験はないでしょうか？

## 違和感

ビルド遅くない？
Preview の表示遅すぎ

今まで Realm 以外にもたくさんのライブラリに依存したアプリ開発が多かったので、たくさんのライブラリに依存しているからそりゃ遅くなるよねっと考えていました。

ただ、最近ビルド速度を計測する方法を覚えたので、個人アプリのビルド速度を測ってみたら驚きの事実が判明しました。

:::note
ビルド速度の計測方法は、以前記事を書いたので、気になる方は見ていただきたい。

https://qiita.com/stotic-dev/items/a34e322e6c2803cbe9ff
:::

ビルドのタイムラインを見てみると、ビルドプロセスのほぼ半分は Realm 関連のコードのコンパイルに使われていたのです。（以下赤枠部分）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/4011fb27-6876-4f03-9daf-f0368a249c1e.png)

ざっくり時間を見てみると、4min ぐらい使っています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/c12d337d-c1db-4771-9584-ba93a78aeccf.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/33f4d5f2-af9d-48cd-bf5e-867c37c22943.png)

この結果を見ると、今まで Realm に依存しているプロジェクトが何だかビルド時間遅い気がしていたのは、Realm によるところが大きいのでは？と思えてきました。

## 仮説

`Realm`から`SwiftData`に移行したら、ビルド速度爆速になるのでは？

## 試してみる

### 検証方法

SPM で、以下構成のサンプルプロジェクトを作って、検証してみます。
それぞれの要素を一つのモジュールと捉えてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/f09edb77-5b36-41aa-9e3a-6f7356ac0dec.png)

:::note
サンプルプロジェクトは Github リポジトリに載せたので、よかったら見てください。

https://github.com/stotic-dev/LocalStorageLibBuildPeformanceExample#
:::

簡単に構成の説明をすると、
`RealmClient`は`RealmSwift`に依存したアプリケーションを実装するモジュールで、このモジュールをビルドすると、`Realm`を含んだアプリをビルドすることになります。

`SwiftDataClient`は`SwiftData`に依存したモジュールで、ビルドすると`SwiftData`を含めてビルドすることになります。

両方で同じ`UI`に依存しているので、それぞれのモジュールで違うのは、どのローカルストレージライブラリに依存しているかだけです。

つまり、`RealmClient`と`SwiftDataClient`のビルド速度を比較することで、今回の仮説を検証できます。

### 検証結果

結果は以下の通りで、`SwiftDataClient`のビルド速度が圧倒的に早かったです。

| モジュール      | 依存ライブラリ | ビルド速度 |
| --------------- | -------------- | ---------- |
| RealmClient     | Realm          | 82.5s      |
| SwiftDataClient | SwiftData      | 2.6s       |

## 仮説②
@externvoid@github (extern void)さんの[コメント](https://qiita.com/stotic-dev/items/bb0e5e164d99fd099732#comment-92043045bf956f2c34ae)を受けて、先ほどの検証は同じ条件ではなくビルド速度の比較方法として適切でなさそうということに気づきました。（コメントありがとうございます！！）

`SwiftData`は標準のSDKにコンパイル済みのバイナリとして同梱されているので、ビルド時にコンパイルは行わないのに対して、`Realm`はSPMでソースコードをチェックアウトしているだけでビルド時にはコンパイルから行わないといけないので、`SwiftData`の方が圧倒的にビルド速度が速くなることは自明でありました。

そのため、同じ条件で（両ライブラリをコンパイル済みのバイナリにして）`RealmClient`と`SwiftDataClient`のビルド速度を比較すると、ほとんど変わらないのではないか？
というコメントをいただきましたので、仮説②として確認してみました！

### 検証方法

SPMで`Realm`に依存するときに、GithubリポジトリのURLを使うのではなく、XCFrameworkを取得して、`binaryTarget`として`Realm`に依存するようにします。

XCFrameworkは、[リポジトリのリリースノート](https://github.com/realm/realm-swift/releases/tag/v20.0.2)から[Carthage用のXCFramework](https://github.com/realm/realm-swift/releases/download/v20.0.2/Carthage.xcframework.zip)を拝借しました。

ダウンロードしたzipを展開して、`Realm.xcframework`と`RealmSwift.xcframework`を`Package.swift`で`binaryTarget`として定義して、依存するようにします。
<details><summary>Package.swift</summary>

```swift
// swift-tools-version: 6.1
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "LocalStoragePeformanceExample",
    platforms: [.iOS(.v18), .macOS(.v15)],
    products: [
        .library(
            name: "SampleUI",
            targets: ["SampleUI"]
        ),
        .library(
            name: "RealmClient",
            targets: ["RealmClient"]
        ),
        .library(
            name: "SwiftDataClient",
            targets: ["SwiftDataClient"]
        )
    ],
    targets: [
        .target(
            name: "SampleUI"
        ),
        .target(
            name: "RealmClient",
            dependencies: [
                "SampleUI",
                "RealmModel"
            ]
        ),
        .target(
            name: "SwiftDataClient",
            dependencies: [
                "SampleUI",
                "SwiftDataModel"
            ]
        ),
        .target(
            name: "RealmModel",
            dependencies: [
                "RealmSwiftBinary",
                "RealmBinary"
            ]
        ),
        .binaryTarget(
            name: "RealmSwiftBinary",
            path: "RealmSwift.xcframework"
        ),
        .binaryTarget(
            name: "RealmBinary",
            path: "Realm.xcframework"
        ),
        .target(name: "SwiftDataModel")
    ]
)
```

</details>

上記手順で、`Realm`をXCFrameworkとして依存することで、コンパイルせずにビルドできるようになるので、`SwiftData`と同じ条件でビルド速度を比較できるようになります。

### 検証結果

| モジュール      | 依存ライブラリ | ビルド速度 |
| --------------- | -------------- | ---------- |
| RealmClient     | Realm          | 1.9s      |
| SwiftDataClient | SwiftData      | 2.6s       |

仮説②の通り、ビルド速度はほぼ同じになりました！！

## 感想

結果で見た通り、`Realm`もコンパイル済みのバイナリを扱うようにすれば、ビルド速度のボトルネックは大幅に解消できそうです。
今回の手順では、XCFrameworkをリポジトリに含める必要があるので、リポジトリのサイズが大きくなってしまう懸念がありますが（別の方法あったら教えてもらえると嬉しいです）
それでもビルド速度の面で受けるメリットはかなり大きいので、このアプローチでビルド速度解消できるケースも多そうと思いました。

~~ビルド速度だけみると、`SwiftData`の方が圧倒的で驚きでした。多少早くなるだろうとはおもってたけど、まさかここまで差が出るとは思わなかったです。~~

~~ただ、ローカルストレージライブラリとしてのパフォーマンス面で比べると
大量のデータに対しての読み書きなどは`Realm`の方が上なので、
パフォーマンス重視のプロダクトには`Realm`を使うといった判断はできそうですね。
パフォーマンスとビルド速度はトレードオフになりそうなので、そこを考慮して今後はローカルストレージのライブラリ選定をしたいです。~~

工夫すれば、`Realm`/`SwiftData`に依存したプロジェクトのビルド速度はほぼ同じなので、
どちらを使うかを検討する時は、ライブラリの機能面やパフォーマンス面を重視する方が良さそうと思いました。

:::note
パフォーマンスの比較は以下記事を参考にしています。

https://www.emergetools.com/blog/posts/swiftdata-vs-realm-performance-comparison
:::