Rork で出力したアプリを実機で動かしたとき、スプラッシュ画面が数秒以上残って不安になる、あるいはスプラッシュが消えた直後に真っ白な画面が一瞬差し込んでから画面が表示される、という相談をよくいただきます。軽微に見えますが、App Store のレビューアーはアプリ起動時の第一印象を厳しく見るため、ここで引っかかってリジェクトされるケースも実際にあります。
症状1: スプラッシュが数秒消えずに止まって見える
もっとも多いのがこれです。原因はほぼ1つで、「SplashScreen.hideAsync() を呼ぶ前に、ビジネスロジックの初期化(ログイン状態の復元・フォント読み込み・リモート設定の取得など)を直列で待たせすぎている」ことです。
Rork の初期テンプレートは以下のようなコードを生成します。一見問題なさそうに見えて、実は await が詰まりがちです。
// ❌ この書き方だと、fetchRemoteConfig が遅いと splash が何秒も残る
import { useEffect } from "react";
import * as SplashScreen from "expo-splash-screen";
import { Stack } from "expo-router";
import { useFonts } from "expo-font";
SplashScreen.preventAutoHideAsync();
export default function RootLayout() {
const [fontsLoaded] = useFonts({
Inter: require("../assets/fonts/Inter.ttf"),
});
useEffect(() => {
(async () => {
if (!fontsLoaded) return;
await fetchRemoteConfig(); // ここが3〜5秒かかる想定
await restoreSession();
await SplashScreen.hideAsync();
})();
}, [fontsLoaded]);
return <Stack />;
}この書き方だと、ネットワークが遅い環境や機内モード直後の起動で、ユーザーからは「アプリが固まっている」ように見えます。修正のポイントは「スプラッシュを隠すトリガーを、UI を描画できる最小条件にする」ことです。
// ✅ UI を出せる最低限が揃ったら即 hide、重い処理は裏で継続
import { useEffect, useState } from "react";
import * as SplashScreen from "expo-splash-screen";
import { Stack } from "expo-router";
import { useFonts } from "expo-font";
SplashScreen.preventAutoHideAsync().catch(() => {});
export default function RootLayout() {
const [fontsLoaded] = useFonts({
Inter: require("../assets/fonts/Inter.ttf"),
});
const [appReady, setAppReady] = useState(false);
useEffect(() => {
if (!fontsLoaded) return;
// 1) UIを描画するのに必須なものだけ待つ(= 何もない)
setAppReady(true);
// 2) 重い処理はバックグラウンドで継続
fetchRemoteConfig().catch((e) => console.warn("remote config失敗:", e));
restoreSession().catch((e) => console.warn("session復元失敗:", e));
}, [fontsLoaded]);
useEffect(() => {
if (appReady) SplashScreen.hideAsync().catch(() => {});
}, [appReady]);
return <Stack />;
}リモート設定やセッション復元が終わっていなくても、最初のスクリーンは「ゲスト状態・デフォルト設定」で表示できる設計にしておくのがコツです。どうしても認証済みの画面しか出せないサービスであれば、<Stack /> の代わりに軽量なローディング画面を1枚挟むと、スプラッシュがずっと残るよりユーザー体験が良くなります。
症状2: スプラッシュ直後に白い画面がチラつく
これは SplashScreen.hideAsync() を呼んだタイミングと、ルート画面が描画されるタイミングの間に隙間ができている状態です。特にダークテーマのアプリで目立ちます。
確認すべきは app.json の backgroundColor と、expo-splash-screen のコンフィグが食い違っていないかです。Rork が出力するテンプレートはしばしば両者が不一致になっています。
// app.json(抜粋)
{
"expo": {
"backgroundColor": "#0B0F14", // ← ルートViewの背景
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#0B0F14" // ← splashの背景。ここを合わせる
},
"ios": {
"backgroundColor": "#0B0F14"
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#0B0F14"
}
}
}
}3つの backgroundColor(ルート・splash・プラットフォーム別)をすべて同じ色にしておくと、スプラッシュが消えた瞬間の白フラッシュが見えなくなります。ライトテーマでも、ホーム画面の背景色と一致させる原則は同じです。
さらに、expo-router のルート _layout.tsx に背景色を明示しておくと保険になります。
import { View } from "react-native";
import { Stack } from "expo-router";
export default function RootLayout() {
return (
<View style={{ flex: 1, backgroundColor: "#0B0F14" }}>
<Stack screenOptions={{ contentStyle: { backgroundColor: "#0B0F14" } }} />
</View>
);
}contentStyle を指定しないと、ナビゲーション遷移の一瞬に React Navigation のデフォルト白背景が透けます。ここで詰まる方が意外に多い箇所です。
症状3: iOS はきれいだが Android だけスプラッシュの画像がずれる
Android 12 以降は SplashScreen API の仕様変更があり、従来の full-screen の画像ではなくアプリアイコン中心の円形スプラッシュに置き換えられています。Rork の初期テンプレートにはこの差分が反映されていないことがあり、Android だけ画像が中央の小さな円に切り抜かれる現象が起きます。
app.json に Android 用の追加設定を入れておくと、端末側の挙動に合わせて調整できます。
{
"expo": {
"plugins": [
[
"expo-splash-screen",
{
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#0B0F14",
"imageWidth": 200
}
]
],
"android": {
"backgroundColor": "#0B0F14",
"splash": {
"image": "./assets/splash-android.png",
"resizeMode": "cover",
"backgroundColor": "#0B0F14"
}
}
}
}私の経験では、スプラッシュ用の画像は「iOS 用にフル解像度(1284×2778px 程度)」と「Android 12+ 用に中央配置の小さめロゴ(1024×1024px・余白多め)」の2枚を用意しておくのが一番失敗しません。同じ1枚を使い回すと、Android 12 以降で円形マスクがかかったときにロゴが大きく欠けてしまいます。
症状4: ダークモードにするとスプラッシュだけ白いままになる
iOS は UIUserInterfaceStyle を設定しないと、端末がダークモードでもスプラッシュは常にライトモードで表示されます。Android も android:theme を分岐させないと同じ現象になります。
Expo では app.json に以下を記述するだけで両プラットフォームのダーク対応が入ります。
{
"expo": {
"userInterfaceStyle": "automatic",
"ios": {
"userInterfaceStyle": "automatic"
},
"android": {
"userInterfaceStyle": "automatic"
},
"splash": {
"image": "./assets/splash-light.png",
"backgroundColor": "#ffffff",
"dark": {
"image": "./assets/splash-dark.png",
"backgroundColor": "#0B0F14"
}
}
}
}splash.dark が効くのは expo-splash-screen を config plugin 経由で使っている場合に限ります。Rork が吐き出す app.json にこのキーがなかったら、上記のように手で追記してください。設定後は必ず npx expo prebuild --clean を実行して native プロジェクトに反映させます。これを忘れると、JS 側だけ書き換えても古いネイティブリソースが残り、ダーク時の背景色が変わりません。
症状の切り分けに使える確認ステップ
最後に、どのパターンに該当するかを判断する簡易フローを置いておきます。
- スプラッシュ表示時間が3秒以上 → 症状1(初期化待ち)を疑う
- スプラッシュが消えた直後の一瞬だけ白い → 症状2(背景色不一致)を疑う
- Android 12+ の実機のみロゴが小さい・切れる → 症状3(Android 12 仕様)を疑う
- ダークモードの端末だけ白背景 → 症状4(dark キー未設定)を疑う
切り分けのために便利なのが、スプラッシュ表示時間の可視化です。_layout.tsx の先頭に以下を仕込んで、実機で何秒かかっているか数値で見ると、症状1なのか別のレイヤーの問題なのかが一目でわかります。
const bootStart = Date.now();
useEffect(() => {
if (appReady) {
console.log("splash shown:", Date.now() - bootStart, "ms");
SplashScreen.hideAsync().catch(() => {});
}
}, [appReady]);500ms 以下なら実質ゼロ体感、1秒を超えると遅いと感じ始め、2秒を超えると「固まっている」と認識されます。この数値をチームで共有しておくと、レビュー前のチェックがぶれなくなります。
実際にハマって時間を溶かした小ネタ
本編に入りきらなかったのですが、自分が過去に時間を溶かした小ネタを3つだけ残しておきます。
1つ目は iOS シミュレーターの splash 画像キャッシュです。splash.png を差し替えてリビルドしても古い画像が残ることがあります。シミュレーターの「Device → Erase All Content and Settings」で初期化し、アプリも一度削除してから npx expo run:ios を走らせ直してください。プロジェクト側の expo prebuild --clean ではシミュレーター側のキャッシュまでは消せません。
2つ目は expo-updates の OTA アップデートが splash を固める件です。起動時の Updates.checkForUpdateAsync() が遅いと、splash の自動非表示も連動して止まります。チェックに上限時間を設けるのが手堅いです。
await Promise.race([
Updates.checkForUpdateAsync(),
new Promise((resolve) => setTimeout(resolve, 2000)),
]);3つ目は Android の端末メーカー独自 UI(One UI や MIUI など)が、アプリのスプラッシュ前に OEM 独自のスプラッシュを挟むことです。コード側では除去できませんが、背景色を OEM の表示と揃えておくと違和感がかなり減ります。
直したあとの最終確認
ここまでの修正を反映したら、npx expo prebuild --clean で native プロジェクトを作り直し、以下の3環境で確認してください。エミュレーターだけだと再現しない差分が意外と多いです。
- iOS 実機のライト / ダーク両モード
- Android 12 以降の実機(Pixel または Galaxy)のダークモード
- 機内モードでの起動(ネットワーク待ちが発生するケース)
スプラッシュ画面は一度整えると半年以上触らない場所なので、今日のうちに4症状を潰しておくと、後々のリリース作業が軽くなります。関連する画面周りの不具合についてはRork アプリで起動直後にクラッシュして白画面になる問題の直し方とRork アプリのローディングが止まる問題のデバッグ手順も合わせて読むと、起動フロー全体の理解が深まります。スプラッシュの意匠そのものを設計から見直したい方にはRork と Expo のスプラッシュ画面 完全設計ガイドが役立つはずです。
まずは一番気になっている症状に絞って、原因特定のための console.log を1行足すところから始めてみてください。数値で見えるようになった瞬間に、どこを直せばいいかが一気にクリアになります。