Rork で作ったアプリを TestFlight に上げる段階で、「アプリサイズが大きすぎます」と App Store Connect から警告が返ってきた、ということはないでしょうか。私も自分で運営している壁紙アプリや癒し系アプリを Rork で作り直す過程で、最初のビルドで 180MB 近くに膨らみ、慌てて削減に取り掛かった経験があります。
結論から書くと、Rork 生成アプリのバイナリサイズは、ソースコードを書き換えなくても 50〜60 % まで縮められるケースがほとんどです。今回はその手順を、私が実機と xcrun ツールでサイズを測りながら調整したときのログとあわせて共有します。
「どこに脂肪が付いているか」をまず切り分ける
最適化は計測から始めないと、体感で「軽くなった気がする」で止まってしまいます。Rork 生成アプリのサイズ構成は、大まかに次の 5 要素に分解できます。
- JavaScript バンドル(
main.jsbundleや Hermes bytecode) - 画像・フォント・動画などのアセット
- Expo モジュールを含むネイティブライブラリのバイナリ
- 言語別リソース(翻訳ファイル・ローカライズアセット)
- デバッグシンボル(
dSYM・未ストリップのネイティブコード)
計測してみると、私の壁紙アプリでは最適化前後で次のような内訳になりました。数字は Xcode Organizer の App Size Report と xcrun での実測値です。
| 構成要素 | 最適化前 | 最適化後 | 主な手段 |
|---|---|---|---|
| 画像アセット | 84 MB | 21 MB | WebP 変換・不要解像度の削除 |
| JavaScript バンドル | 12 MB | 8 MB | moment→dayjs・lodash の個別 import |
| ネイティブライブラリ | 48 MB | 34 MB | 未使用 Expo モジュール除去・ABI splits |
| 言語リソース | 6 MB | 4 MB | 未使用ロケールの削除 |
| デバッグシンボル | 22 MB | 2 MB | dSYM をアップロードから分離・strip |
| 合計 | 172 MB | 69 MB(-60%) | — |
この表で伝えたいのは、削減の伸びしろは要素ごとに大きく違うということです。画像で 63 MB 削れた一方、言語リソースは 2 MB。まず「いちばん重い一要素」に手を入れると、同じ労力でも効き方がまるで違います。
iOS であれば Xcode の Organizer → Archives → Download App Size Report で、App Thinning 後の実ダウンロードサイズと原因別内訳を確認できます。Android では ./gradlew :app:bundleRelease 後に生成される output-metadata.json や APK Analyzer でモジュール別の占有量を確認します。「体感で大きい」ではなく「画像アセットが 72MB」という数字に落として初めて、削減計画が立てられます。
画像アセットを「最適なフォーマット」で入れ直す
サイズが肥大化している Rork アプリの 7 割以上で、最大の原因は画像アセットです。Rork はプロンプトに合わせて高品質なプレースホルダー画像を生成してくれるため便利ですが、PNG のままバンドルに同梱されていると、壁紙系・癒し系のような画像主体アプリでは 100MB 超えがあっという間です。
私が実際に使っている削減手順は次の 3 段構えです。
# ① アセットを一括で WebP / AVIF に変換(SVG はそのまま残す)
# cwebp は brew install webp で入れられます
find assets/images -type f \( -name "*.png" -o -name "*.jpg" \) -print0 \
| xargs -0 -I {} cwebp -q 82 "{}" -o "{}.webp"
# ② 変換後のサイズを比較(オリジナル vs WebP)
du -sh assets/images/**/*.png assets/images/**/*.webp | sort -h | tail -20
# ③ 元ファイルを削除し、コード側の参照をまとめて置換
find assets/images -type f \( -name "*.png" -o -name "*.jpg" \) -delete
rg -l "\.png\"" src/ | xargs sed -i '' 's/\.png"/\.webp"/g'期待する出力として、私のアプリでは画像アセット合計 84MB → 21MB(-75%)まで減りました。React Native は標準で WebP を扱えますが、Android で透過 PNG をフラットに変換すると壊れるケースがあるため、透過が必要なアイコン類は PNG のまま残すのが無難です。
画像フォーマットの選択でさらに踏み込みたい場合は、WebP と AVIF のデコードコストと CDN ネゴシエーション に、フォーマットごとの伸長コストと配信側での出し分けをまとめています。
JavaScript バンドルを「何が重いのか」まで可視化する
画像を整理してもまだ大きい場合、次に疑うべきは JavaScript バンドルに混入している巨大な依存ライブラリです。Expo SDK 55 以降では npx expo export --dump-sourcemap で出力されたソースマップを react-native-bundle-visualizer に通すと、モジュール単位での占有量が視覚化できます。
# バンドル可視化のセットアップ
npm install -D react-native-bundle-visualizer
# 本番相当のバンドルを解析
npx react-native-bundle-visualizer --platform ios --dev falseこれで lodash(全取り込みしていると 70KB 超)や moment(約 230KB、タイムゾーンデータ込みで 400KB 超)といった「重い常連」が浮かび上がります。lodash は lodash-es への差し替えか個別インポート、moment は date-fns や dayjs への置き換えで、合計 500KB 以上削れることも珍しくありません。
注意点として、Rork で生成された直後のプロジェクトには不要な Babel プラグインが残っていることがあります。babel.config.js に @babel/preset-env が含まれている場合は、React Native 標準の babel-preset-expo に統一することで、ビルド後のバンドルから数十 KB が消えます。
Hermes と Proguard でネイティブレイヤーを絞る
iOS では Hermes の有効化(Expo SDK 55 ではデフォルト)を確認し、Android では enableProguardInReleaseBuilds = true と enableShrinkResourcesInReleaseBuilds = true の両方を android/app/build.gradle に入れておくのが基本です。
// android/app/build.gradle
android {
buildTypes {
release {
shrinkResources true
minifyEnabled true
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
}
}
}Android 側では ABI splits を有効にすると、arm64-v8a 単独の APK を生成でき、ユーザーが実際にダウンロードするサイズが半分以下になります。Google Play なら App Bundle(AAB)にするだけで、Play 側が端末ごとに最適化してくれますが、社内配布の APK を作る場合は ABI splits の方が効果があります。
App Thinning と On-Demand Resources でダウンロードサイズを下げる
ここまで来ても App Store Connect 上で「アプリの全体サイズ」がまだ大きい場合、iOS であれば Assets.xcassets を使い、App Thinning を最大限効かせます。Rork が出力するアセットはフラットファイルになりがちですが、壁紙アプリのように端末種別ごとに違う解像度で提供したい画像は、Asset Catalog に登録して 1x / 2x / 3x を分けておくと、ユーザー端末にはその端末向けの 1 セットだけが配信されます。
On-Demand Resources は少し踏み込んだ仕組みで、「初回起動時は必要最小限だけ入れ、チュートリアル中に残りをダウンロードする」といった運用ができます。教育系アプリやゲーム系アプリでは効果が大きいですが、実装コストもそれなりに高いため、まずは App Thinning の範囲で様子を見るのが現実的です。
この辺りの設計思想については、Rork アプリの起動時間を短くする実践テクニック で、起動・描画との合わせ技として扱っています。サイズと起動を両輪で見ると、ユーザー体験の改善幅が一段上がります。
バイナリサイズと OTA 更新サイズは別物として測る
ここまでは「初回インストールで転送されるバイナリ」を縮める話でした。運用フェーズに入ると、もうひとつ別のサイズ指標が効いてきます。OTA(Over-the-Air)更新で配信される差分の大きさです。
Expo SDK 55 の EAS Update は、Hermes バイトコードの差分だけを配信する方式に変わりました。私のアプリでは、機能追加のたびに数 MB の JS バンドル全体を配っていたものが、変更分だけの配信になり、更新ペイロードが 4 分の 1 近くまで縮みました。転送量が減るぶん、バックグラウンドでの適用がほぼ即座になり、EAS プランの帯域消費も抑えられます。
ここで気をつけたいのは、バイナリを削っても OTA 更新は自動では軽くならないという点です。更新のたびに巨大な画像を新規同梱すれば、差分は当然ふくらみます。初回バイナリは Asset Catalog と App Thinning で、継続的な更新は「JS とアセットの差分をいかに小さく保つか」で、別々に最適化するのが実務的です。OTA で足りる変更か本体ビルドが必要かは、Expo の fingerprint ツールで機械的に判定できます。
削っても跳ね返ってきた三つの落とし穴
サイズ削減で厄介なのは、数字が減ったのに別のところが壊れる、という形で問題が遅れて表に出てくることです。ビルドは通り、審査も通り、リリースしてから気づく。私が実際に踏んだものを三つ挙げます。
shrinkResources で動的参照のリソースが消える。 リソース名を文字列で組み立てて getIdentifier() で引いている箇所があると、Gradle は「どこからも参照されていない」と判断して削ります。ビルドは通り、該当画面を開いた瞬間だけ落ちます。壁紙アプリのカテゴリアイコンがこれで消えました。保持したいリソースは明示します。
<!-- android/app/src/main/res/raw/keep.xml -->
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@drawable/category_*,@raw/onboarding_*"
tools:shrinkMode="strict" />tools:shrinkMode="strict" を付けておくと、明示したもの以外は落とす前提になるため、後から追加したリソースの取りこぼしに気づきやすくなります。
dSYM をアーカイブから外すと、クラッシュログが読めなくなる。 表の 22 MB → 2 MB はここで稼いだ部分ですが、シンボルファイルを「削除」してしまうと Crashlytics のスタックトレースが 16 進アドレスの羅列になります。外すのではなく、アップロード対象から分離して別途保管する、が正解です。
# アーカイブから dSYM だけ取り出して保管し、バイナリからは strip する
cp -R MyApp.xcarchive/dSYMs "$HOME/dsyms/$(date +%Y%m%d)-$(git rev-parse --short HEAD)"
# Crashlytics へアップロード(保管したものを指定)
./Pods/FirebaseCrashlytics/upload-symbols -gsp GoogleService-Info.plist -p ios "$HOME/dsyms/..."私はコミットハッシュ付きのディレクトリ名で残すようにしてから、「どのビルドの dSYM か分からない」という一番不毛な調査をしなくて済むようになりました。
Asset Catalog に入れた画像は OTA で差し替えられない。 App Thinning を効かせるために画像を Assets.xcassets へ移すと、その画像はネイティブバイナリ側の資産になります。つまり EAS Update では更新できません。季節ごとに絵柄を差し替えたい画像まで Asset Catalog に入れてしまい、次の入れ替えで本体ビルドの提出が必要になった、というのが私の失敗でした。
判断の線引きはシンプルにしています。アプリの寿命のあいだ変わらないもの(アイコン・ロゴ・恒久的な背景)は Asset Catalog へ。運用で差し替える可能性があるものはバンドル側に残し、可能なら CDN 配信に寄せる。この一本の線を引いておくだけで、後から効いてきます。
削減の結果は「配信サイズ」で確かめる
最後に確認の話です。手元の .ipa のファイルサイズを見て安心するのは早いところがあります。ユーザーが実際にダウンロードするのは App Thinning 後の、その端末向けの一組だけだからです。手元の 120 MB が実配信では 58 MB、ということが普通に起こります。
iOS は Xcode Organizer の App Size Report が正確ですが、提出前に手元で概算したいときは、エクスポートした成果物に付いてくるレポートを見るのが早いです。
# Ad Hoc / App Store エクスポート時に生成されるサイズレポートを読む
cat build/export/App\ Thinning\ Size\ Report.txt | grep -A3 "App size"Android は AAB から端末別 APK を実際に生成して測るのが確実です。
# bundletool で端末構成ごとの配信サイズを一覧化する
bundletool build-apks --bundle=app-release.aab --output=app.apks \
--ks=release.keystore --ks-key-alias=release
bundletool get-size total --apks=app.apks --dimensions=ABI,SDKget-size total は端末構成ごとの最小・最大ダウンロードサイズを返します。私はこの出力を削減前後で保存し、差分を記録するようにしました。数字が残っていると、次に同じ作業をするときの見積もりが一気に楽になります。
明日の一手をどこに置くか
バイナリサイズの削減は、一度で劇的に減らそうとするより、計測 → 画像 → バンドル → ネイティブ層の順で、各工程を 20〜40% ずつ削るつもりで進めた方が結果的に速く進みます。順番を守ると、どの施策がどれだけ効いたかが数字で分離できるからです。
明日やることを一つだけ挙げるなら、Xcode Organizer の App Size Report を開いて、自分のアプリの「いちばん重い一要素」を特定することです。画像が最重量なら WebP 変換で 3 桁 MB が動くこともありますし、ネイティブ層が重いなら ABI splits と AAB の方が先です。重い順から手を付ける、それだけで削減の速度はまるで変わります。
サイズが小さくなると、審査の警告が消えるだけでなく、低速回線のユーザーがインストールを途中で諦めなくなります。最初のダウンロードは、ユーザーがアプリに払う最初のコストです。そこを軽くしておくことは、まだ会っていない誰かへの小さな配慮だと思っています。
計測の数字が動いたら、ぜひその差分を記録に残してみてください。次の一本を作るときの、いちばん確かな見積もりになります。