Bridge のまま残されたアプリに何が起きるのか
React Native は2024年末から新アーキテクチャ(New Architecture)がデフォルト有効となりました。Expo SDK 52 以降でも同様に新アーキテクチャが標準となり、古いブリッジ(Bridge)ベースの実装を引き続き使うアプリは、パフォーマンス上の恩恵を受けられないだけでなく、将来的なサポート終了リスクを抱えることになります。
新アーキテクチャの中核を成す3つの技術スタック:
- JSI(JavaScript Interface): JS と Native が直接メモリを共有し、非同期ブリッジのオーバーヘッドを排除
- TurboModules: 必要な時にだけネイティブモジュールを遅延ロードし、起動時間を短縮
- Fabric: UIレンダリングエンジンの再設計。スレッドセーフな同期レンダリングが可能に
しかし移行の壁は高く、特に「独自ネイティブモジュールを持つアプリ」では手作業での対応が大量に発生します。ここではAntigravity を活用してこの移行を体系的・効率的に進める方法を、実際のコードを交えて詳解します。
対象読者:
- Expo SDK 50/51 でアプリを運営しており、SDK 52+ への移行を検討している開発者
- 独自ネイティブモジュールを持ち、TurboModules 化に苦労している方
- Expo Router v3 の file-based routing を最大活用したいチームリーダー
移行前の現状分析:Antigravity に全体像を把握させる
移行作業の最初のステップは「現状把握」です。Antigravity のプロジェクト解析機能を使って、移行コストの見積もりと影響範囲を洗い出しましょう。
プロジェクト全体のアーキテクチャ分析
Antigravity に以下のように依頼します:
@Workspace このReact Nativeプロジェクトの新アーキテクチャ移行準備状況を分析して。
具体的に:
1. 旧来のNativeModules/NativeEventEmitterを使っているファイルを全列挙
2. 新アーキテクチャ非対応の可能性があるnpmパッケージを特定
3. 移行の難易度を低/中/高で分類したロードマップを作成
Antigravity は package.json と全ソースファイルをスキャンし、以下のような分析レポートを返します:
【移行分析レポート】
高難度(手動対応必須):
- src/modules/BleManager.ts → NativeModules.BleManager 直接参照あり
- src/modules/CameraModule.ts → NativeEventEmitter 使用中
- android/app/src/main/java/.../CameraModule.java → 旧Bridge実装
中難度(ライブラリ更新で対応可能):
- react-native-camera: v4.2.0 → v5.x.x へ更新で新アーキ対応
- react-native-bluetooth-classic: 未対応。代替ライブラリを検討
低難度(設定変更のみ):
- react-native-async-storage: 1.21+ で新アーキ対応済み
- react-native-vector-icons: 10.x で対応済み
このレポートを元に移行を3フェーズに分けるのが Antigravity 推奨のアプローチです。
フェーズ1:Expo SDK 52 へのアップグレードと新アーキテクチャ有効化
1-1. package.json の更新
Antigravity のターミナルコマンド提案機能を使います:
# Antigravityにコマンドを生成させる
# "Expo SDK 52への移行コマンドを生成して" と指示
npx expo install expo@^52.0.0 --fix
npx expo install react-native@0.76.0 --fixexpo install --fix は依存関係の不整合を自動検出・修正するため、手動でのバージョン調整作業を大幅に削減できます。
1-2. app.json / app.config.ts の設定変更
新アーキテクチャを明示的に有効化します:
{
"expo": {
"name": "MyApp",
"newArchEnabled": true,
"android": {
"newArchEnabled": true
},
"ios": {
"newArchEnabled": true
}
}
}app.config.ts を使っている場合は型安全に記述できます:
// app.config.ts
import { ExpoConfig, ConfigContext } from 'expo/config';
export default ({ config }: ConfigContext): ExpoConfig => ({
...config,
name: 'MyApp',
slug: 'my-app',
newArchEnabled: true,
// 環境変数で切り替え可能にする
extra: {
newArchEnabled: process.env.NEW_ARCH_ENABLED !== 'false',
},
});Antigravity に「この設定を環境変数でフィーチャーフラグ化して」と指示すると、上記のようなコードを即座に生成してくれます。
フェーズ2:TurboModules への移行
新アーキテクチャの中心的な変更点が TurboModules です。既存の NativeModules ベースのコードを型安全な TurboModules へ移行する方法を解説します。
2-1. Codegen スキーマの定義
TurboModules は TypeScript で型定義を書き、Codegen が Swift/Kotlin コードを自動生成する仕組みです。
まず、型定義ファイルを作成します:
// src/specs/NativeCameraModule.ts
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
export interface Spec extends TurboModule {
// カメラの許可状態を取得
requestCameraPermission(): Promise<'granted' | 'denied' | 'blocked'>;
// 写真を撮影
takePicture(options: {
quality: number; // 0.0 ~ 1.0
base64: boolean;
exif: boolean;
}): Promise<{
uri: string;
width: number;
height: number;
base64?: string;
exif?: Record<string, unknown>;
}>;
// フラッシュモードの設定
setFlashMode(mode: 'on' | 'off' | 'auto' | 'torch'): void;
// イベントリスナー(新アーキ対応)
addListener(eventName: string): void;
removeListeners(count: number): void;
}
export default TurboModuleRegistry.getEnforcing<Spec>('CameraModule');Antigravity に「既存のCameraModule.tsからTurboModule仕様のspecファイルを生成して」と依頼すると、既存コードを解析しながら上記のような型定義を自動生成します。
2-2. package.json への Codegen 設定追加
{
"name": "my-camera-module",
"codegenConfig": {
"name": "CameraModuleSpecs",
"type": "modules",
"jsSrcsDir": "./src/specs",
"android": {
"javaPackageName": "com.myapp.camera"
}
}
}2-3. iOS 側の実装(Swift)
Codegen が生成したプロトコルに準拠する Swift 実装:
// ios/CameraModule.swift
import Foundation
import AVFoundation
@objc(CameraModule)
class CameraModule: NSObject, NativeCameraModuleSpec {
// セッション管理
private var captureSession: AVCaptureSession?
private var photoOutput: AVCapturePhotoOutput?
@objc func requestCameraPermission(
_ resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock
) {
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
resolve("granted")
case .denied, .restricted:
resolve("denied")
case .notDetermined:
AVCaptureDevice.requestAccess(for: .video) { granted in
resolve(granted ? "granted" : "denied")
}
@unknown default:
resolve("denied")
}
}
@objc func takePicture(
_ options: NSDictionary,
resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock
) {
guard let output = photoOutput else {
reject("NO_SESSION", "Camera session not initialized", nil)
return
}
let settings = AVCapturePhotoSettings()
let quality = options["quality"] as? Double ?? 0.8
// フォトキャプチャ処理(省略)
// Antigravityに "AVFoundation でHEIF/JPEG撮影実装して" と依頼すれば
// 完全な実装コードを生成してもらえる
_ = quality
resolve(["uri": "file:///tmp/photo.jpg", "width": 1920, "height": 1080])
}
@objc func setFlashMode(_ mode: String) {
// フラッシュモード実装
}
// TurboModules対応の必須メソッド
@objc func addListener(_ eventName: String) {}
@objc func removeListeners(_ count: Double) {}
}2-4. Android 側の実装(Kotlin)
// android/app/src/main/java/com/myapp/CameraModule.kt
package com.myapp
import com.facebook.react.bridge.*
import com.facebook.react.module.annotations.ReactModule
import android.Manifest
import android.content.pm.PackageManager
import androidx.core.app.ActivityCompat
@ReactModule(name = CameraModule.NAME)
class CameraModule(reactContext: ReactApplicationContext) :
NativeCameraModuleSpec(reactContext) {
companion object {
const val NAME = "CameraModule"
}
override fun getName(): String = NAME
override fun requestCameraPermission(promise: Promise) {
val context = reactApplicationContext
val hasPermission = ActivityCompat.checkSelfPermission(
context,
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED
if (hasPermission) {
promise.resolve("granted")
} else {
// パーミッションリクエスト処理
// Antigravityにreact-native-permissionsとの統合実装を依頼可能
promise.resolve("denied")
}
}
override fun takePicture(options: ReadableMap, promise: Promise) {
try {
val quality = options.getDouble("quality")
val includeBase64 = options.getBoolean("base64")
// Camera2 API または CameraX を使って撮影
// Antigravityに "CameraX APIを使った高品質撮影実装" を依頼
val result = WritableNativeMap().apply {
putString("uri", "file:///sdcard/photo.jpg")
putInt("width", 1920)
putInt("height", 1080)
}
promise.resolve(result)
} catch (e: Exception) {
promise.reject("CAPTURE_ERROR", e.message, e)
}
}
override fun setFlashMode(mode: String) {
// フラッシュモード切り替え実装
}
override fun addListener(eventName: String) {}
override fun removeListeners(count: Double) {}
}フェーズ3:Expo Router v3 の新機能を最大活用する
Expo Router v3 は単なるナビゲーションライブラリを超え、フルスタックフレームワークへと進化しました。
3-1. Server Components と API Routes
Expo Router v3 では React Server Components と API Routes が使えます:
app/
├── _layout.tsx ← ルートレイアウト
├── (tabs)/
│ ├── _layout.tsx ← タブレイアウト
│ ├── index.tsx ← ホームタブ
│ └── profile.tsx ← プロフィールタブ
├── api/
│ └── users+api.ts ← API Route(サーバーサイド)
├── modal.tsx ← モーダル画面
└── [id].tsx ← 動的ルート
API Route の実装例:
// app/api/users+api.ts
import { ExpoRequest, ExpoResponse } from 'expo-router/server';
export async function GET(request: ExpoRequest): Promise<ExpoResponse> {
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get('page') ?? '1');
// データベースから取得(Supabase/Firebase/Prisma等)
const users = await fetchUsers({ page, limit: 20 });
return ExpoResponse.json({
users,
pagination: {
page,
total: users.length,
hasMore: users.length === 20,
},
});
}
export async function POST(request: ExpoRequest): Promise<ExpoResponse> {
const body = await request.json();
// バリデーション
if (!body.email || !body.name) {
return ExpoResponse.json(
{ error: 'email and name are required' },
{ status: 400 }
);
}
const user = await createUser(body);
return ExpoResponse.json(user, { status: 201 });
}Antigravity に「このアプリのユーザー認証APIをExpo RouterのAPI Routesで実装して。JWTトークン管理も含めて」と依頼すると、Supabase Auth や Firebase Auth との統合コードまで生成してくれます。
3-2. Typed Routes による型安全なナビゲーション
Expo Router v3 の Typed Routes で、ナビゲーション先の型エラーをコンパイル時に検出できます:
// tsconfig.json に追加
{
"compilerOptions": {
"types": ["expo-router/types"]
}
}
// app.json に追加
{
"expo": {
"experiments": {
"typedRoutes": true
}
}
}// 使用例:型安全なナビゲーション
import { router, Link } from 'expo-router';
// ✅ コンパイル時に型チェックされる
router.push('/profile');
router.push({ pathname: '/[id]', params: { id: '123' } });
// ❌ 存在しないルートはコンパイルエラーになる
// router.push('/nonexistent'); // Type Error!
// Link コンポーネントも型安全
<Link href="/profile">プロフィール</Link>
<Link href={{ pathname: '/[id]', params: { id: userId } }}>
ユーザー詳細
</Link>3-3. Layouts と Slots の高度な活用
// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { useColorScheme } from 'react-native';
import { Platform } from 'react-native';
export default function TabLayout() {
const colorScheme = useColorScheme();
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: colorScheme === 'dark' ? '#fff' : '#000',
// iOS 26のビジュアルデザインに対応
tabBarStyle: Platform.select({
ios: { position: 'absolute' },
default: {},
}),
headerShown: false,
}}
>
<Tabs.Screen
name="index"
options={{
title: 'ホーム',
tabBarIcon: ({ color }) => (
// Antigravityにアイコンコンポーネントの生成を依頼
<HomeIcon color={color} />
),
}}
/>
<Tabs.Screen
name="camera"
options={{ title: 'カメラ' }}
/>
<Tabs.Screen
name="profile"
options={{ title: 'プロフィール' }}
/>
</Tabs>
);
}Antigravity × EAS Build:CI/CD パイプラインの完全自動化
4-1. eas.json の最適化
{
"cli": {
"version": ">= 10.0.0",
"appVersionSource": "remote"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"ios": {
"simulator": true
},
"env": {
"APP_ENV": "development"
}
},
"preview": {
"distribution": "internal",
"channel": "preview",
"env": {
"APP_ENV": "staging"
}
},
"production": {
"autoIncrement": true,
"channel": "production",
"env": {
"APP_ENV": "production"
},
"ios": {
"resourceClass": "m-medium"
},
"android": {
"buildType": "apk",
"resourceClass": "medium"
}
}
},
"submit": {
"production": {
"ios": {
"appleId": "your@apple.com",
"ascAppId": "1234567890",
"appleTeamId": "ABCDEFGHIJ"
},
"android": {
"serviceAccountKeyPath": "./google-services-key.json",
"track": "production"
}
}
}
}4-2. GitHub Actions との連携
Antigravity に「EAS BuildをGitHub Actionsに統合するワークフローを作成して」と依頼すると、以下のような完全なCIパイプラインを生成します:
# .github/workflows/eas-build.yml
name: EAS Build & Submit
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
name: EAS Build
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Setup Expo
uses: expo/expo-github-action@v8
with:
expo-version: latest
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
# PRの場合はpreviewビルド
- name: Build (Preview)
if: github.event_name == 'pull_request'
run: eas build --profile preview --platform all --non-interactive
# mainブランチへのマージ時はproductionビルド
- name: Build (Production)
if: github.ref == 'refs/heads/main'
run: eas build --profile production --platform all --non-interactive
# production ビルド時はApp Storeへ自動提出
- name: Submit to Stores
if: github.ref == 'refs/heads/main'
run: eas submit --platform all --non-interactive
env:
EXPO_APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}4-3. OTA(Over The Air)アップデートの戦略的活用
// src/hooks/useOtaUpdate.ts
import { useEffect, useState } from 'react';
import * as Updates from 'expo-updates';
import { Alert } from 'react-native';
export function useOtaUpdate() {
const [isUpdateAvailable, setIsUpdateAvailable] = useState(false);
const [isUpdating, setIsUpdating] = useState(false);
useEffect(() => {
if (__DEV__) return; // 開発環境ではスキップ
async function checkForUpdates() {
try {
const update = await Updates.checkForUpdateAsync();
if (update.isAvailable) {
setIsUpdateAvailable(true);
// バックグラウンドでダウンロード開始
await Updates.fetchUpdateAsync();
Alert.alert(
'アップデートが利用可能',
'新しいバージョンが準備できました。今すぐ再起動しますか?',
[
{ text: 'あとで', style: 'cancel' },
{
text: '再起動',
onPress: async () => {
setIsUpdating(true);
await Updates.reloadAsync();
},
},
]
);
}
} catch (error) {
console.warn('OTA update check failed:', error);
}
}
checkForUpdates();
}, []);
return { isUpdateAvailable, isUpdating };
}よくあるエラーと Antigravity を使った解決法
5-1. "TurboModule not found" エラー
Error: Cannot find native module 'CameraModule'
原因: Codegen が正しく実行されていない、またはネイティブビルドが古い。
Antigravity への依頼:
"TurboModule not found" エラーの原因を分析して。
Podfile と build.gradle、package.json の codegenConfig を確認して
問題箇所と修正方法を教えて。
一般的な修正手順:
# iOS
cd ios && pod install --repo-update && cd ..
# Android
cd android && ./gradlew clean && cd ..
# ビルドキャッシュのクリア
npx expo prebuild --clean5-2. Fabric レンダラーの互換性問題
新アーキテクチャ有効後に ScrollView や FlatList の挙動が変わるケースがあります。
// 旧アーキテクチャ時代のコード(問題が起きやすい)
<FlatList
data={items}
renderItem={({ item }) => <ItemComponent item={item} />}
// Fabricでは onEndReachedThreshold の挙動が変わる
onEndReachedThreshold={0.5}
onEndReached={loadMore}
/>
// 新アーキテクチャ対応版
<FlashList // @shopify/flash-list を使う(Fabricネイティブ最適化済み)
data={items}
renderItem={({ item }) => <ItemComponent item={item} />}
estimatedItemSize={80} // 必須:パフォーマンス最適化のヒント
onEndReachedThreshold={0.5}
onEndReached={loadMore}
/>Antigravity に「FlatListをFlashListに移行して。型定義も含めてリファクタリングして」と依頼すると、プロジェクト全体のFlatListを自動的にFlashListに置き換えます。
5-3. Metro バンドラーの設定エラー
// metro.config.js
const { getDefaultConfig } = require('expo/metro-config');
const config = getDefaultConfig(__dirname);
// 新アーキテクチャ対応設定
config.resolver.unstable_enablePackageExports = true;
// シンボリックリンク対応(monorepo環境)
config.resolver.nodeModulesPaths = [
path.resolve(__dirname, 'node_modules'),
];
module.exports = config;パフォーマンス計測:移行前後の比較
移行の効果を可視化するために、Antigravity と Flashlight(React Native パフォーマンス測定ツール)を組み合わせます:
# Flashlight のインストール
npm install -g @perf-tools/flashlight
# 測定実行
flashlight measure --app com.myapp --duration 30実際のプロジェクトでの改善例(参考値):
- 起動時間: 平均 2.8秒 → 1.9秒(約32%短縮)
- FPS(スクロール時): 平均 52fps → 58fps(Fabric による同期レンダリング効果)
- 初回レンダリング: 340ms → 220ms(TurboModules の遅延ロード効果)
- メモリ使用量: 245MB → 198MB(約19%削減)
Antigravity に「Flashlightの計測結果JSONを分析して、ボトルネックと改善提案をリストアップして」と依頼すると、計測結果を解析して優先度付きの改善リストを生成します。
まとめ
React Native 新アーキテクチャへの移行は、一度に全てを変えようとすると失敗しやすい大規模な作業です。本記事で紹介したアプローチを振り返ると:
- Antigravity によるコード分析で移行コストを可視化し、3フェーズに分割
- TurboModules 化では Codegen と Antigravity の共同作業で Swift/Kotlin コードを効率生成
- Expo Router v3 の Server Components・API Routes・Typed Routes で開発体験を大幅向上
- EAS Build + GitHub Actions で本番リリースを完全自動化
Antigravity は特に「既存コードの解析と移行計画策定」「プラットフォームをまたいだコード生成」において真価を発揮します。まずは小さなモジュールひとつから TurboModules 化を試し、Antigravity とのペアプログラミングで移行の感触を掴んでみてください。
React Native 開発のさらなる深掘りには、Expo × Antigravity クロスプラットフォーム開発完全ガイドや、Antigravity × Fastlane App Store 自動デプロイ攻略も合わせてご参照ください。
実際のプロファイリング手法と最適化パターンが豊富に収録されており、本記事の内容をさらに深める良書です。