Rork で作ったアプリに「社内ツールの画面を WebView でそのまま埋め込みたい」「外部サービスのログインを一時的に WebView で挟みたい」といった要件を入れた途端、シミュレーターでは動いていたのに実機で真っ白、ということがよくあります。私自身も、プレビュー動画用に急いで組み込んだ WebView が Android の実機で一切描画されず、リリース直前に半日潰した苦い記憶があります。
WebView の「真っ白」はエラーが前面に出ないぶん原因の切り分けが難しいのですが、実際に起きる詰まり方には明確なパターンがあります。ここでは Rork で react-native-webview を使う前提で、現場でよく遭遇する 6 つの典型パターンを上から順に潰していく流れで整理します。
まずは「どこで止まっているか」を切り分ける
真っ白に見えるとき、実は 3 つの異なる状態が混ざっています。
- ネイティブ側の
WebViewコンポーネント自体が画面に乗っていない WebViewは乗っているが URL がロードに失敗している- URL は読み込めているが HTML 側が空、または JavaScript で描画待ちのまま止まっている
この切り分けを最初にやるかどうかで、その後の作業時間が 1 時間変わります。いちばん手堅いのは、onLoadStart / onLoad / onError をすべて仕込んで、どのイベントが発火しているかをログに出してしまうことです。
// components/DebugWebView.tsx
import { WebView, type WebViewNavigation } from "react-native-webview";
import { View, Text, StyleSheet } from "react-native";
import { useState } from "react";
type Props = { uri: string };
export default function DebugWebView({ uri }: Props) {
const [status, setStatus] = useState<"idle" | "loading" | "loaded" | "error">("idle");
const [lastUrl, setLastUrl] = useState<string>("");
const [errorText, setErrorText] = useState<string>("");
return (
<View style={styles.container}>
{/* ステータスバー:ユーザーには見せず、開発中のみ描画する */}
<Text style={styles.status}>
status: {status} / url: {lastUrl}
{errorText ? ` / error: ${errorText}` : ""}
</Text>
<WebView
source={{ uri }}
style={styles.webview}
onLoadStart={(e) => {
setStatus("loading");
setLastUrl(e.nativeEvent.url);
}}
onLoad={() => setStatus("loaded")}
onError={(e) => {
setStatus("error");
setErrorText(e.nativeEvent.description ?? "unknown");
}}
onNavigationStateChange={(nav: WebViewNavigation) => {
// 遷移が起きた瞬間の URL を確認できる
setLastUrl(nav.url);
}}
/>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
status: { padding: 8, backgroundColor: "#111", color: "#0f0", fontSize: 12 },
webview: { flex: 1 },
});このデバッグビューを 1 分だけ挟むと、「loading のまま止まっている」のか「error になっている」のか、「loaded なのに真っ白」なのかが一目で分かります。以降の原因特定はこの結果をもとに進めます。
原因 1:iOS の App Transport Security(ATS)で HTTPS が必須
iOS 実機で真っ白になり、シミュレーターでも onError が発火するのに Android では動く ─ これはほぼ確実に ATS による HTTP ブロックです。http:// で始まる URL は iOS 側で黙ってブロックされ、何のダイアログも出ません。
Rork 経由でエクスポートした Expo プロジェクトの場合、app.json の ios.infoPlist に例外を書くか、先方の URL を HTTPS に切り替えてもらうのがまっとうな解決策です。社内サーバーなど一時的に HTTP を許容したい場合は、次のように最小限のドメイン単位で許可するとリジェクトリスクを抑えられます。
{
"expo": {
"ios": {
"infoPlist": {
"NSAppTransportSecurity": {
"NSAllowsArbitraryLoads": false,
"NSExceptionDomains": {
"staging.example.internal": {
"NSTemporaryExceptionAllowsInsecureHTTPLoads": true,
"NSTemporaryExceptionMinimumTLSVersion": "TLSv1.0",
"NSIncludesSubdomains": true
}
}
}
}
}
}
}NSAllowsArbitraryLoads を true にする解説記事も見かけますが、App Store 審査で理由を問われることがあるので、対象ドメインだけを NSExceptionDomains で開ける形を強くおすすめします。HTTPS 化・証明書まわりで詰まったときは、API 通信側と同じ原因であることも多いので、ネットワークリクエストのデバッグ手順 もあわせて確認すると早いです。
原因 2:Android の cleartextTraffic と Mixed Content
Android 9(API 28)以降は HTTP 通信がデフォルトで無効になっています。iOS と同じように、HTTPS が使えるならそれが最良ですが、社内 URL や開発用の IP アドレス(http://192.168.x.x)を開きたい場面は現実に残ります。
Expo 管理下のプロジェクトでは、app.json の android.usesCleartextTraffic を true にすることでまず扉を開けます。そのうえで、react-native-webview 側では mixedContentMode を明示しておかないと、HTTPS ページ内の HTTP リソース(画像や iframe)が読み込まれず、結果として中身がスカスカになります。
<WebView
source={{ uri: "http://192.168.10.5:3000" }}
mixedContentMode="compatibility" // "always" / "compatibility" / "never"
javaScriptEnabled
domStorageEnabled
// 本番で使う場合は該当 URL のみに限定する
/>mixedContentMode="always" を本番ビルドにそのまま残すとセキュリティ上よくないので、開発ビルドと本番ビルドで分けておくのが無難です。EAS のプロファイル別に環境変数で切り替える運用が、Rork で作ったアプリでもいちばん扱いやすいと感じます。
原因 3:リダイレクトとクッキーでログイン画面が無限ループ
外部サービスの OAuth や SAML を WebView で踏ませたとき、「ログインボタンを押した直後に最初の画面に戻る」「ずっとローディングのまま」になることがあります。これはクッキーが WebView 内で保持されていない、またはリダイレクト先が about:blank に化けているケースがほとんどです。
最低限押さえておきたいのは以下の 3 点です。
sharedCookiesEnabledをtrueにして、iOS のネイティブクッキーストアと WebView を共有するthirdPartyCookiesEnabledを Android でtrueにしておく(iOS 14 以降の ITP 相当の挙動に注意)onShouldStartLoadWithRequestでカスタムスキーム(yourapp://callback)をフックしてネイティブ側へ戻す
<WebView
source={{ uri: loginStartUrl }}
sharedCookiesEnabled
thirdPartyCookiesEnabled
incognito={false}
onShouldStartLoadWithRequest={(req) => {
if (req.url.startsWith("yourapp://callback")) {
// カスタムスキームを検出したらネイティブ側の認証完了処理へ渡す
handleAuthCallback(req.url);
return false; // WebView には遷移させない
}
return true;
}}
/>onShouldStartLoadWithRequest を仕込まずにカスタムスキームを渡すと、WebView が「開けない URL」と判断して白画面になることがあります。OAuth 絡みで真っ白になるときは、まずここを疑うのが近道です。
原因 4:JavaScript は動いているのに描画待ちで止まっている
「onLoad まで発火したのに真っ白」というパターンは、SPA 側が window.requestIdleCallback やフォントロードを待ってから描画している、もしくは WebView の viewport メタタグがなくて縦横比がおかしくなっているだけ、というのが定番です。
確認は injectedJavaScriptBeforeContentLoaded で document.readyState と document.body.innerHTML.length を通知させるのが速いです。
<WebView
source={{ uri }}
onMessage={(e) => console.log("[webview]", e.nativeEvent.data)}
injectedJavaScriptBeforeContentLoaded={`
window.addEventListener('DOMContentLoaded', () => {
window.ReactNativeWebView.postMessage(JSON.stringify({
ready: document.readyState,
bodyLength: document.body ? document.body.innerHTML.length : 0,
title: document.title,
}));
});
true; // React Native 側で評価結果を undefined にするおまじない
`}
/>bodyLength がそれなりの数字なのに画面が真っ白なら、CSS の height: 100vh が iOS の Safari 同等で計算されている、あるいはダークテーマで背景が黒のまま文字も黒になっている、という見落としが疑われます。
なお表示は出るのに画像だけ出ない、という症状なら原因はまた別で、画像・メディアの読み込みエラー対処 に網羅的にまとめてあります。
原因 5:プロセス外 WebView のクラッシュ(Android 限定)
Android では 2023 年以降、WebView がアプリ本体と別プロセスで動くようになり、メモリ不足や System WebView の更新中に勝手に「真っ白+無反応」状態になることがあります。onRenderProcessGone が実装されていないと、その瞬間アプリ側は何も知りません。
対策はハンドラを書いてリロードする、これだけです。
<WebView
source={{ uri }}
onRenderProcessGone={(e) => {
console.warn("[webview] render process gone", e.nativeEvent);
// 自動リロード、またはエラー画面に切り替え
webRef.current?.reload();
}}
/>体感としては低価格帯の Android 端末で再現しやすく、レビュー欄に「急に画面が動かなくなる」と書かれる症状の多くがこれです。気づかずに放置すると星が削られるので、出荷前に一度仕込んでおく価値があります。長時間使うアプリで頻発する場合は、より広いクラッシュ対策を扱った アプリが実機でクラッシュするときの切り分け も参考にしてください。
原因 6:Rork プレビュー特有の「Companion では見えるのに本番で出ない」
Rork で開発中、Companion 経由のプレビューでは完璧に動いていたのに、TestFlight ビルドだと WebView だけ真っ白 ─ これは Companion が一部のネイティブ設定を甘めに上書きしているためです。
チェックすべきは以下の順序です。
app.jsonのplugins配列にreact-native-webviewが追加されているかeas.jsonの開発プロファイルと本番プロファイルでusesCleartextTraffic/infoPlistの値がずれていないか- 本番ビルドは Release 用の ATS ルールが適用される。Debug 用に緩めていた例外が Release に入っていない可能性がある
eas build --profile preview --platform ios でプレビューに近いビルドを作り、そのビルドで再現するかを確認すると、Companion 固有か本番固有かを切り分けられます。
次に何をするか
WebView の「真っ白」はエラーが見えにくいだけで、切り分けるイベントを 1 本仕込めば原因は必ず特定できます。いま手元のアプリで真っ白になっている方は、まず上の DebugWebView コンポーネントをそのまま貼り付け、status と errorText がどう遷移するかを 1 回観察してみてください。そこから原因 1 〜 6 のどの節に飛べばよいかが決まります。
WebView を使い込むなら、React Native と Expo のネットワーク周りの知識が土台になります。