RORK LABEN
TOOLING — Rorkの開発者向けリポジトリが動き続けています。rork-xcodeが7月16日、rork-deviceが7月15日、rork-plistが7月13日に更新されましたOPUS46 — RorkでClaude Opus 4.6が稼働しています。Rork MaxはClaude Codeを土台にアプリを組み立てる設計ですSIM — ブラウザ上で動くクラウドのiOSシミュレータを備え、実機へのインストールは1クリック、App Storeへの提出は2クリックと案内されていますMAX — Rork MaxはReact Nativeではなく純粋なSwiftを出力します。iPhone・iPad・Apple Watch・Apple TV・Vision Pro、そしてiMessageまでが射程ですNATIVE — HealthKit、ARKitとLiDAR、NFC、Dynamic Island、Live Activities、Metalによる3D、Core MLのオンデバイス推論まで扱えますSEED — RorkはLeft Lane Capitalが主導する1,500万ドルのシードラウンドを実施し、Peak XVとa16z Speedrunが参加しましたTOOLING — Rorkの開発者向けリポジトリが動き続けています。rork-xcodeが7月16日、rork-deviceが7月15日、rork-plistが7月13日に更新されましたOPUS46 — RorkでClaude Opus 4.6が稼働しています。Rork MaxはClaude Codeを土台にアプリを組み立てる設計ですSIM — ブラウザ上で動くクラウドのiOSシミュレータを備え、実機へのインストールは1クリック、App Storeへの提出は2クリックと案内されていますMAX — Rork MaxはReact Nativeではなく純粋なSwiftを出力します。iPhone・iPad・Apple Watch・Apple TV・Vision Pro、そしてiMessageまでが射程ですNATIVE — HealthKit、ARKitとLiDAR、NFC、Dynamic Island、Live Activities、Metalによる3D、Core MLのオンデバイス推論まで扱えますSEED — RorkはLeft Lane Capitalが主導する1,500万ドルのシードラウンドを実施し、Peak XVとa16z Speedrunが参加しました
記事一覧/開発ツール
開発ツール/2026-04-22中級

Rork アプリでカスタムフォントを美しく読み込む — 起動時のチラつき(FOIT/FOUT)を防ぐ expo-font 実装

Rork で生成したアプリにカスタムフォントを導入する際、起動時に一瞬デフォルトフォントが見えてしまう『FOIT / FOUT』現象。expo-font と SplashScreen を組み合わせて、読み込み完了までスプラッシュを保持する実装パターンを具体的なコードで解説します。

Rork515expo-fontカスタムフォントSplashScreenFOITFOUTReact Native209

Rork で AI にプロンプトを投げて完成したアプリを実機で試したとき、起動直後の一瞬だけフォントが「システムのデフォルト書体」で表示され、その後にカスタムフォントに置き換わる挙動を見たことはないでしょうか。Web でよく言われる FOIT(Flash of Invisible Text)や FOUT(Flash of Unstyled Text)と同じ現象が、モバイルアプリの起動時にも起きます。

Rork のプロンプトで「Noto Sans JP を使ってください」とお願いしても、expo-font の読み込みタイミングを明示しなければ、この一瞬のチラつきが残ってしまうことが多いです。ここではexpo-font と expo-splash-screen を組み合わせて、フォント読み込みが完了するまでスプラッシュ画面を保持するという、地味ですがユーザー体験に直結する実装パターンをご紹介します。

この「一瞬のチラつき」は何が起きているのか

Rork が生成するアプリは Expo + React Native の構成ですから、JavaScript の評価とネイティブ描画の間には必ずラグがあります。カスタムフォントの場合、ざっくり以下の順序で処理が進みます。

  1. ネイティブ層がスプラッシュ画面を表示する
  2. JavaScript バンドルが読み込まれ、React ツリーが評価される
  3. useFonts フックなどがフォントファイルを非同期で読み込む
  4. React のルートコンポーネントが描画される
  5. フォント読み込みが完了し、テキストが再レンダリングされる

問題は 4 と 5 の間です。フォントが読み込まれる前にテキストが描画されると、一瞬だけ OS のシステムフォント(iOS なら San Francisco、Android なら Roboto)で表示され、その後カスタムフォントに差し替わります。ユーザーの目には「画面が一瞬瞬いた」ように見えるのです。

この問題は fontFamily をプロンプトで指示するだけでは解決しません。読み込み完了までスプラッシュを維持するという、描画タイミングそのものの制御が必要になります。

解決の全体像 — expo-splash-screen を「手動で閉じる」

Expo のデフォルトでは、スプラッシュ画面は React のルートコンポーネントがマウントされた瞬間に自動で閉じられます。これを「フォントが読み込まれるまで待ってから手動で閉じる」方式に変更すれば、チラつきは原理的に発生しません。

必要なパッケージは以下の2つです。Rork が生成したプロジェクトであれば expo-font はほぼ確実に入っているはずですが、expo-splash-screen は明示的にインストールが必要な場合があります。

# Rork プロジェクトのルートで実行
npx expo install expo-font expo-splash-screen

Rork のチャット上で「expo-splash-screen を使ってフォント読み込み完了まで待機する実装に書き換えてください」と依頼すると、以下に似たコードが生成されます。ただし、細部の堅牢性まで AI 任せにすると落とし穴があるため、生成後に必ず手で見直すのが私の流儀です。

最小の実装例 — _layout.tsx に集約する

Expo Router を使う Rork プロジェクトなら、フォント読み込みは app/_layout.tsx に集約するのが素直です。以下は動作確認済みのサンプルで、フォント読み込み中はスプラッシュを維持し、完了後に手動で閉じます。

// app/_layout.tsx
import { Stack } from "expo-router";
import { useFonts } from "expo-font";
import * as SplashScreen from "expo-splash-screen";
import { useEffect } from "react";
import { View } from "react-native";
 
// 1. SplashScreen の自動非表示を止める
//    これを呼ばないと、React マウント直後にスプラッシュが消えてしまう
SplashScreen.preventAutoHideAsync().catch(() => {
  // 二重呼び出しや WebView 環境では無視してよい
});
 
export default function RootLayout() {
  // 2. カスタムフォントを登録する
  const [fontsLoaded, fontError] = useFonts({
    "NotoSansJP-Regular": require("../assets/fonts/NotoSansJP-Regular.ttf"),
    "NotoSansJP-Bold": require("../assets/fonts/NotoSansJP-Bold.ttf"),
  });
 
  // 3. フォント準備完了 or エラー時にスプラッシュを閉じる
  useEffect(() => {
    if (fontsLoaded || fontError) {
      SplashScreen.hideAsync();
    }
  }, [fontsLoaded, fontError]);
 
  // 4. 準備完了まで null を返す(描画しない)
  if (!fontsLoaded && !fontError) {
    return null;
  }
 
  return (
    <View style={{ flex: 1 }}>
      <Stack screenOptions={{ headerShown: false }} />
    </View>
  );
}

このコードの肝は 4 の「フォントが準備できるまで null を返す」 部分です。描画そのものを遅延させることで、システムフォントで一瞬表示される隙を物理的になくしています。fontError を判定に入れているのは、万が一フォントが壊れていても、スプラッシュで固まらず画面に進めるようにするためです。

期待する動作

  • 起動時: スプラッシュが通常より長めに(フォント読み込み分 + 100〜300ms 程度)表示される
  • スプラッシュが消えた瞬間: 最初の画面がカスタムフォントで描画される
  • フォント読み込みに失敗した場合: スプラッシュが閉じ、システムフォントで画面が表示される(クラッシュはしない)

つまずきやすいポイントと対処

1. require のパスを間違える

assets/fonts/ に置いたつもりが、実際は assets/font/ だった、というような typo は Metro のバンドル時にエラーを吐いて起動が止まります。iOS シミュレータでは Unable to resolve module と出ますので、パスを素直に見直してください。

私の経験では、フォントファイル名のハイフンとアンダースコアの差異(Noto-Sans-JP vs NotoSansJP)で詰まることが最も多いです。Google Fonts からダウンロードすると NotoSansJP-Regular.ttf の形式で来るはずですから、そのままの名前で useFonts のキーとファイル名を揃えるのが安全です。

2. fontFamily の指定を「ファイル名」にしてしまう

よくある間違いは、<Text style={{ fontFamily: "NotoSansJP-Regular.ttf" }}> のように拡張子まで書いてしまうことです。useFonts に渡したキーの文字列を使うのが正解ですから、以下のように書きます。

<Text style={{ fontFamily: "NotoSansJP-Regular", fontSize: 16 }}>
  こんにちは、カスタムフォントの世界へ
</Text>

Rork に「このテキストを Noto Sans JP Regular で表示してください」と日本語で指示すると、ときどき拡張子付きで生成されることがあります。生成後に grep -n "\.ttf\"" app/ のようにチェックする癖をつけると未然に防げます。

3. iOS のビルドキャッシュが古いフォントを掴む

開発中にフォントを差し替えたのに、シミュレータでは古いフォントのまま表示される、という症状に出会ったら、ほぼキャッシュが原因です。以下の順番で試すと、だいたい解消します。

  • Metro キャッシュをクリア: npx expo start --clear
  • シミュレータのホーム画面からアプリを一度削除して再インストール
  • ios/buildandroid/build を削除して再ビルド(Bare workflow の場合)

4. スプラッシュが永遠に消えない

useFonts[false, undefined] のまま止まっている、というケースです。原因の9割はフォントファイルのパスが解決できず、expo-font が内部でエラーを投げて fontError に入っているのに、useEffect の条件で拾えていないパターンです。

開発中は一時的に以下を入れて、どちらの経路で抜けたかログを確認するのがおすすめです。

useEffect(() => {
  if (fontsLoaded) console.log("[Font] loaded OK");
  if (fontError) console.log("[Font] error:", fontError);
  if (fontsLoaded || fontError) {
    SplashScreen.hideAsync();
  }
}, [fontsLoaded, fontError]);

Rork のプロンプトで指示する場合のコツ

Rork のチャットに「カスタムフォントを使いたい」と伝えるときは、以下の4点を1つのプロンプトに含めると、ほぼ一発で上記のパターンを生成してくれます。

  • 使いたいフォントファミリー名と weight(例: Noto Sans JP の Regular と Bold)
  • フォントファイルの配置場所(例: assets/fonts/
  • expo-splash-screen でフォント読み込み完了まで待機したい旨
  • _layout.tsx に集約してほしい旨

私がよく使うプロンプトの雛形はこんな形です。

Noto Sans JP の Regular と Bold を assets/fonts/ に置いたので、app/_layout.tsx でそれらを useFonts で読み込み、expo-splash-screen を使って読み込み完了までスプラッシュを維持してください。フォント読み込み前は null を返して描画を遅延させ、FOUT を防ぎたいです。

生成結果が気に入らなければ、「fontError を条件に含めてください」「preventAutoHideAsync のエラーは無視してください」など、差分だけを短く追加すると精度が上がります。Rork との対話は、一度に全部伝えるよりも「足りない所を小さく足していく」のが私の好みです。

Android で追加で気をつけたいこと

iOS は Metro がフォントをバンドルしてくれますが、Android の一部の端末(特に中華系のベンダーで独自のカスタム ROM を載せているモデル)では、fontFamily の解決が不安定なケースに遭遇したことがあります。その場合は、Text コンポーネントをラップする共通コンポーネントを作り、allowFontScalingfontFamily を明示的に指定すると安定しやすいです。

import { Text, TextProps } from "react-native";
 
export function AppText(props: TextProps) {
  return (
    <Text
      allowFontScaling={true}
      {...props}
      style={[
        { fontFamily: "NotoSansJP-Regular", color: "#222" },
        props.style,
      ]}
    />
  );
}

全画面で <Text> を直接使わず <AppText> 経由にしておくと、後からフォントを差し替えたくなったときも1箇所の修正で済みます。

5. 開発ビルドでは正しいが、EAS のプロダクションビルドでシステムフォントに戻る

開発時には狙ったフォントで表示されるのに、EAS でビルドしたバイナリだけシステムフォントに戻ってしまうことがあります。経験上、以下のどれかに当てはまるケースがほとんどでした。

  • フォントファイルが assets/fonts/ ではなく、独自のパスに置かれている
  • app.json または app.config.jsassetBundlePatterns でフォントフォルダを除外してしまっている
  • .gitignoreassets/fonts/*.ttf を除外してしまい、CI のビルド時にフォントが存在しない

私は一度、フォントライセンスチェックのスクリプトを走らせた際に .gitignore にフォントを追加してしまい、半日溶かしたことがあります。git ls-files assets/fonts/ で手元にリストが出るか、最初に確認するのが一番早い予防策です。

CJK フォントで気になるバンドルサイズを減らす

Noto Sans JP は美しいフォントですが、Regular 単体でも約 5 MB あります。Regular と Bold を両方バンドルすると 10 MB 近く増え、20〜30 MB 規模のアプリではインストールサイズに無視できない影響が出ます。

私が現場でよく使う対処は2つです。

グリフのサブセット化: 使うグリフが限られている場合(例: 特定のテンプレート文言しか出てこないアプリ)、fonttoolspyftsubset で使うグリフだけに絞ったフォントを作れます。大きさが10分の1程度になることも珍しくありません。

# 使う Unicode のリストを作成したうえでサブセット化
pip install fonttools
pyftsubset NotoSansJP-Regular.ttf   --unicodes-file=used_unicodes.txt   --output-file=NotoSansJP-Regular.subset.ttf

差し替えるだけでバンドルが劇的に軽くなります。ただし、UI コピーを追加するたびにサブセットを作り直す必要がある点だけは覚えておいてください。

遅延読み込み(Lazy load): たまにしか開かれない画面だけに使うフォントなら、CDN から起動後にダウンロードする手もあります。expo-font は URL 指定の Font.loadAsync({ Name: { uri: "https://..." } }) をサポートしているので、初期バンドルを軽く保ちながら必要な時だけフォントを追加できます。

複数プロジェクトで私が落ち着いた小さなお作法

Rork のプロジェクトをいくつか公開するうちに、フォント周りで「これは守っておくと未来の自分が楽」というお作法が固まってきたので、参考までにご紹介します。

  • カスタムフォントは assets/fonts/ に平置き(サブフォルダを作らない)
  • Weight は Regular と Bold の2種類に絞る(デザイン要件が明確でない限り、増やすとバンドルだけ膨らむ)
  • Text は最初から AppText にラップしておく(たとえ最初は props をそのまま流すだけでも、後で一括変更するときに救われる)
  • フォント読み込みのロジックは _layout.tsx 一箇所に集約(各画面で useFonts を呼ぶと、ある日必ずどこかでガードを忘れる)

次の一歩

まずは手元の Rork プロジェクトで、ここに載せた _layout.tsx を貼り替えて、起動時のチラつきが本当に消えるかをシミュレータで確認してみてください。効果の違いは一目でわかるはずです。そのうえで、アプリ全体のタイポグラフィをカスタムフォントに統一していくと、Rork が生成した「それっぽい」UI が、自分のブランドらしいアプリへと変わっていきます。

Rork の UI 品質を底上げする周辺テクニックは、Rork 生成アプリの『安っぽさ』を消すプロンプト や Rork アニメーション実装ガイド でも掘り下げていますので、あわせて読んでいただけると嬉しいです。

シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

開発ツール2026-07-15
「、」が行頭に落ちた — Rork が組んだ名言カードと日本語の禁則処理
React Native の Text は日本語の行頭禁則を保証しません。実測した違反率、WORD JOINER による事前結合の実装、onTextLayout での自動監査、Rork Max(SwiftUI)との挙動差までまとめました。
開発ツール2026-07-15
Rorkの癒しアプリにスリープタイマーを付けたら、フェードアウトが実際の音とズレた話
Rorkが生成するスリープタイマーは、壁時計のsetIntervalで音量を下げます。数値上は30分でも、実際の音はもっと早く消えたり不自然に途切れたりします。フェードを再生位置で駆動し直す実装と、対数カーブ・停止処理・画面ロック時の限界までを実運用の視点でまとめました。
開発ツール2026-07-14
AI再生成に壊されない設計 — Rork の生成コードと手書きロジックを分ける境界の引き方
Rork へ追加指示を出すたびに手書きのロジックが消える。プロンプトで守るのは対症療法です。生成コードと自分の所有コードを層で分け、再生成が届かない境界を設計で引く方法を、Zustand と service 層の実装例つきでお伝えします。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →