mcpField
F
flutter-pre-caching

flutter-pre-caching

github.com/evanca開発663195 回インストール3 週間前に更新
リポジトリ

flutter-pre-caching について

Flutter と Flutter Web の開発者向けプリキャッシュガイドです。近く表示されるフォント、画像、設定、データを先読みし、初回描画の遅延やカクつきを抑える方法を説明します。

主な機能

  1. 01フォントと画像の先読み
  2. 02起動時の設定とデータのウォームアップ
  3. 03Lottie と Rive のアセット読み込み
  4. 04Flutter Web のブラウザ先読み
  5. 05直近の表示内容に応じたキャッシュ

活用シーン

  1. 01初回画面のフォント切り替えや画像のちらつきの軽減
  2. 02起動に必要なリソースの先読み
  3. 03初回画面のアニメーション表示の高速化
  4. 04Flutter Web の重要アセットの先読み計画

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/evanca/flutter-ai-rules --skill flutter-pre-caching

インストール後、Claude Code で/flutter-pre-cachingを入力して読み込まれたか確認します。

詳細

Flutter と Flutter Web での事前キャッシュ

事前キャッシュを行うと、カクつき、読み込み時のちらつき、フォントの切り替わり、初回描画の遅延を防げます。

基本ルール:

ユーザーが次の1〜2画面で目にする可能性が高いものだけを事前キャッシュします。

アプリ全体を事前キャッシュしないでください。起動が遅くなり、メモリを無駄に消費する可能性があります。


1. Google Fonts

実行時の Google Fonts の事前読み込み

GoogleFonts.pendingFonts() を使って、テキストを表示する前にフォントのバリエーションを読み込みます。

import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';

class ExampleSimple extends StatefulWidget {
  const ExampleSimple({super.key});

  @override
  State<ExampleSimple> createState() => _ExampleSimpleState();
}

class _ExampleSimpleState extends State<ExampleSimple> {
  late final Future<List<void>> googleFontsPending;

  @override
  void initState() {
    super.initState();

    googleFontsPending = GoogleFonts.pendingFonts([
      GoogleFonts.poppins(),
      GoogleFonts.montserrat(fontStyle: FontStyle.italic),
    ]);
  }

  @override
  Widget build(BuildContext context) {
    final pushButtonTextStyle = GoogleFonts.poppins(
      textStyle: Theme.of(context).textTheme.headlineMedium,
    );

    final counterTextStyle = GoogleFonts.montserrat(
      fontStyle: FontStyle.italic,
      textStyle: Theme.of(context).textTheme.displayLarge,
    );

    return FutureBuilder<List<void>>(
      future: googleFontsPending,
      builder: (context, snapshot) {
        if (snapshot.connectionState != ConnectionState.done) {
          return const SizedBox();
        }

        return Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'You have pushed the button this many times:',
              style: pushButtonTextStyle,
            ),
            Text(
              '0',
              style: counterTextStyle,
            ),
          ],
        );
      },
    );
  }
}
本番環境での推奨事項

本番環境とオフライン対応では、重要なフォントをアセットとしてバンドルすることをおすすめします。

flutter:
  fonts:
    - family: AppFont
      fonts:
        - asset: assets/fonts/AppFont-Regular.ttf
        - asset: assets/fonts/AppFont-Bold.ttf
          weight: 700

アプリのテーマで使用します:

MaterialApp(
  theme: ThemeData(
    fontFamily: 'AppFont',
  ),
)

2. アセット画像

すぐに表示される画像には precacheImage を使います。

@override
void didChangeDependencies() {
  super.didChangeDependencies();

  precacheImage(
    const AssetImage('assets/images/header.png'),
    context,
  );
}

その後、通常どおり画像を使用します:

Image.asset('assets/images/header.png')

3. ネットワーク画像

NetworkImage とともに precacheImage を使います。

@override
void didChangeDependencies() {
  super.didChangeDependencies();

  precacheImage(
    const NetworkImage('https://example.com/image.png'),
    context,
  );
}

その後、通常どおり使用します:

Image.network('https://example.com/image.png')

重要: precacheImage は Flutter のメモリ内画像キャッシュをウォームアップします。長期的なオフラインキャッシュを提供するものではありません。

ディスクキャッシュには、cached_network_image などのパッケージを使います:

dependencies:
  cached_network_image: ^latest

例:

CachedNetworkImage(
  imageUrl: 'https://example.com/image.png',
)

4. 複数画像の事前読み込み

@override
void didChangeDependencies() {
  super.didChangeDependencies();

  final images = <ImageProvider>[
    const AssetImage('assets/images/header.png'),
    const AssetImage('assets/images/avatar.png'),
    const NetworkImage('https://example.com/banner.png'),
  ];

  for (final image in images) {
    precacheImage(image, context);
  }
}

5. 画像の準備ができるまで待機する

precacheImage は Future<void> を返すため、実際の UI を描画する前に待機できます。

late Future<void> _preloadImagesFuture;

@override
void didChangeDependencies() {
  super.didChangeDependencies();

  _preloadImagesFuture = Future.wait([
    precacheImage(
      const AssetImage('assets/images/header.png'),
      context,
    ),
    precacheImage(
      const NetworkImage('https://example.com/banner.png'),
      context,
    ),
  ]);
}

@override
Widget build(BuildContext context) {
  return FutureBuilder<void>(
    future: _preloadImagesFuture,
    builder: (context, snapshot) {
      if (snapshot.connectionState != ConnectionState.done) {
        return const CircularProgressIndicator();
      }

      return Column(
        children: [
          Image.asset('assets/images/header.png'),
          Image.network('https://example.com/banner.png'),
        ],
      );
    },
  );
}

メモ

precacheImage で context が必要な場合は、initState ではなく didChangeDependencies を使います。


6. アプリ起動時に通常、何を事前キャッシュすべきですか?

適した候補:

  • アプリのロゴ
  • 最初の画面のヒーロー画像
  • 最初に表示される背景画像
  • 現在のユーザーのアバター
  • 最初に表示されるカード/リストの画像
  • メインのフォントのバリエーション
  • 小さなローカル設定ファイル
  • 初回描画に必要な翻訳
  • 最初の API リクエスト
  • すぐに表示される Lottie/Rive アニメーション

適さない候補:

  • すべての商品画像
  • すべてのギャラリー画像
  • すべてのリモートフィード画像
  • アプリ内のすべてのアイコン
  • すべてのルートの画像
  • すべての画面にあるすべての画像
  • すぐには表示されない大きなアニメーション

7. 実用的なアプリ起動時のプリローダー

class AppPreloader {
  const AppPreloader();

  Future<void> preload(BuildContext context) async {
    await Future.wait([
      _precacheImages(context),
      _loadCriticalAssets(),
      _warmUpInitialData(),
    ]);
  }

  Future<void> _precacheImages(BuildContext context) {
    return Future.wait([
      precacheImage(
        const AssetImage('assets/images/home_hero.png'),
        context,
      ),
      precacheImage(
        const AssetImage('assets/images/logo.png'),
        context,
      ),
    ]);
  }

  Future<void> _loadCriticalAssets() async {
    await rootBundle.loadString('assets/config/app_config.json');
  }

  Future<void> _warmUpInitialData() async {
    // Example:
    // await userRepository.getCurrentUser();
  }
}

使用方法:

late Future<void> _preloadFuture;

@override
void didChangeDependencies() {
  super.didChangeDependencies();

  _preloadFuture = const AppPreloader().preload(context);
}

FutureBuilder を使う場合:

@override
Widget build(BuildContext context) {
  return FutureBuilder<void>(
    future: _preloadFuture,
    builder: (context, snapshot) {
      if (snapshot.connectionState != ConnectionState.done) {
        return const SplashScreen();
      }

      return const HomeScreen();
    },
  );
}

8. ローカル JSON/設定の事前読み込み

機能フラグ、ローカルアプリ設定、モックデータ、翻訳、オンボーディングコンテンツなどに便利です。

import 'package:flutter/services.dart';

final configJson = await rootBundle.loadString(
  'assets/config/app_config.json',
);

例:

class AppConfigLoader {
  Future<String> load() {
    return rootBundle.loadString('assets/config/app_config.json');
  }
}

9. 初回 API リクエストのウォームアップ

Flutter の画像キャッシュという意味での事前キャッシュではありませんが、起動時の最適化として最も有用なことがよくあります。

final userFuture = userRepository.getCurrentUser();
final dashboardFuture = dashboardRepository.getDashboard();

例:

class StartupData {
  const StartupData({
    required this.user,
    required this.dashboard,
  });

  final User user;
  final Dashboard dashboard;
}

Future<StartupData> loadStartupData() async {
  final results = await Future.wait([
    userRepository.getCurrentUser(),
    dashboardRepository.getDashboard(),
  ]);

  return StartupData(
    user: results[0] as User,
    dashboard: results[1] as Dashboard,
  );
}

10. Lottie の事前読み込み

アニメーションがすぐに表示される場合は、事前に読み込みます。

final composition = await AssetLottie(
  'assets/animations/success.json',
).load();

これは早い段階で表示されるアニメーションにのみ使用してください。アプリ起動時に大きなアニメーションを多数読み込むのは避けてください。


11. Rive の事前読み込み

ビルド中にアセットの読み込みを開始するとカクつきが発生するため、代わりに RiveFile のバイトデータを事前に読み込みます:

import 'package:flutter/services.dart';
import 'package:rive/rive.dart';

// Preload the RiveFile
final data = await rootBundle.load('assets/animations/character.riv');
final riveFile = RiveFile.import(data);

// Display using direct constructor to avoid initialization delays:
RiveAnimation.direct(riveFile);

これにより、ウィジェットがマウントされた直後にアニメーションが描画されます。


12. シェーダーのウォームアップ

ほとんどのアプリでは必要ありません。次のような場合に役立つことがあります:

  • 負荷の高いカスタムペイント
  • 複雑なトランジション
  • 既知のアニメーションのカクつき
  • 古いレンダリング経路

基本形:

class CustomShaderWarmUp extends ShaderWarmUp {
  @override
  void warmUpOnCanvas(Canvas canvas) {
    final paint = Paint();

    canvas.drawRect(
      const Rect.fromLTWH(0, 0, 100, 100),
      paint,
    );
  }
}

次に、runApp の前に設定します:

void main() {
  PaintingBinding.instance.shaderWarmUp = CustomShaderWarmUp();

  runApp(const MyApp());
}

プロファイリングでシェーダーコンパイルによるカクつきが確認された場合にのみ使用してください。


13. Flutter Web:考え方の違い

Flutter Web では、次の考え方を控えめにします:

  • すべてをメモリに事前キャッシュする

次のことをより重視します:

  • 重要なファイルをブラウザーが早めにダウンロードするようにする
  • 最初の画面で使うアセットのみ Flutter のキャッシュをウォームアップする
  • フォントのフォールバックによる予期しないダウンロードを避ける
  • 初回描画の前に巨大なアセットを読み込まない

14. Flutter Web での画像の事前読み込み

最初の画面で重要な画像には、ブラウザーのプリロードと Flutter の precacheImage の両方を使います。

web/index.html で:

<link
  rel="preload"
  href="assets/assets/images/home_hero.png"
  as="image"
>

Flutter で:

await precacheImage(
  const AssetImage('assets/images/home_hero.png'),
  context,
);

両方使う理由:

  • HTML のプリロードによって、ブラウザーでのダウンロードがより早く始まります。
  • precacheImage によって、Flutter の画像キャッシュがウォームアップされます。

15. Flutter Web でのネットワーク画像の事前読み込み

web/index.html で:

<link
  rel="preload"
  href="https://example.com/banner.png"
  as="image"
>

Flutter で:

await precacheImage(
  const NetworkImage('https://example.com/banner.png'),
  context,
);

ユーザーがほぼ確実に目にする画像にのみ使用してください。


16. Flutter Web のフォントのプリロード

pubspec.yaml でフォントを宣言します。

flutter:
  fonts:
    - family: AppFont
      fonts:
        - asset: assets/fonts/AppFont-Regular.ttf
        - asset: assets/fonts/AppFont-Bold.ttf
          weight: 700

web/index.html で重要なフォントをプリロードします。

<link
  rel="preload"
  href="assets/assets/fonts/AppFont-Regular.ttf"
  as="font"
  type="font/ttf"
  crossorigin
>

Flutter で使用します。

ThemeData(
  fontFamily: 'AppFont',
)

通常は、次のフォントをプリロードします。

  • レギュラー
  • ボールド
  • 必要に応じてミディアム

最初の画面で必要な場合を除き、すべてのウェイトやイタリック体をプリロードするのは避けてください。


17. Flutter Web と絵文字

絵文字を使うと、フォントのフォールバック処理が発生することがあります。

例:

Text('Welcome 👋 🎉')

Web では、次の問題が発生することがあります。

  • 追加のフォントダウンロード
  • レンダリングの遅延
  • フォールバックフォントへの切り替え
  • 空白の四角形
  • プラットフォームごとに異なる絵文字のスタイル

18. オプション 1:最初の描画では絵文字を避ける

シンプルで効果的な方法です。

Text('Welcome')

アプリの読み込み後に絵文字を表示します。

Text('Welcome 👋')

絵文字が装飾目的であれば、多くの場合、これが最適な方法です。


19. オプション 2:絵文字フォントをバンドルしてプリロードする

pubspec.yaml にフォントを追加します。

flutter:
  fonts:
    - family: NotoColorEmoji
      fonts:
        - asset: assets/fonts/NotoColorEmoji.ttf

web/index.html でプリロードします。

<link
  rel="preload"
  href="assets/assets/fonts/NotoColorEmoji.ttf"
  as="font"
  type="font/ttf"
  crossorigin
>

必要な箇所で使用します。

const Text(
  '👋 🎉 ❤️',
  style: TextStyle(
    fontFamily: 'NotoColorEmoji',
  ),
)

重要:

  • 絵文字フォントはサイズが大きいことがあります。
  • 絵文字が製品にとって重要でない限り、巨大な絵文字フォントをバンドルしないでください。

20. 絵文字のプリロードが適している場合

適しているケース:

  • チャットアプリ
  • リアクション
  • コメント
  • ソーシャル UI
  • 絵文字ピッカー
  • 絵文字を多用するオンボーディング
  • ブランドで絵文字を多用している

適していないケース:

  • ウェルカム画面にある装飾目的の絵文字が 1 つ
  • ボタンにあるランダムな絵文字
  • アプリの奥深くでしか使わない、まれな絵文字

ルール:

  • 絵文字が装飾目的なら、表示を遅らせるか、最初の描画から外してください。
  • 絵文字が製品の中核となる場合は、実績のある絵文字のレンダリング方法を使用してください。

21. Flutter Web のレンダラーファイル

Flutter Web の起動時には、CanvasKit や Skwasm などのレンダラーアセットが含まれることがあります。

通常、これらを Dart からプリロードする必要はありません。Flutter Web の初期化を通じてレンダラーを設定します。

Web のブートストラップでの例:

_flutter.loader.load({
  config: {
    renderer: 'canvaskit',
  },
});

ほかの設定には、次のようなものが含まれることがあります。

_flutter.loader.load({
  config: {
    renderer: 'canvaskit',
    canvasKitBaseUrl: '/canvaskit/',
  },
});

ホスティングまたはパフォーマンス上の明確な理由がない限り、ほとんどのアプリでは Flutter に任せてください。


22. Flutter Web 起動時の実用的なリスト

通常はプリロードするもの:

  • アプリのレギュラー/ボールドフォント
  • ロゴ
  • 最初のヒーロー画像
  • 最初に表示される背景画像
  • ファーストビューに必要な重要なアセット画像
  • 最初の描画で絵文字が中核となる場合に限り、絵文字フォント
  • すぐに必要となる小さな設定ファイル

通常は避けるもの:

  • すべての絵文字フォント
  • すべての製品画像
  • すべてのルート画像
  • すべてのリモートアバター
  • すぐに表示されない大きな Lottie/Rive ファイル
  • すべてのフォントウェイト
  • アセット内のすべての画像

23. 推奨フォルダー構成

assets/
  images/
    logo.png
    home_hero.png
    onboarding_hero.png

  fonts/
    AppFont-Regular.ttf
    AppFont-Bold.ttf
    NotoColorEmoji.ttf

  config/
    app_config.json

  animations/
    success.json
    character.riv

24. pubspec.yaml の例

flutter:
  assets:
    - assets/images/
    - assets/config/
    - assets/animations/

  fonts:
    - family: AppFont
      fonts:
        - asset: assets/fonts/AppFont-Regular.ttf
        - asset: assets/fonts/AppFont-Bold.ttf
          weight: 700

    - family: NotoColorEmoji
      fonts:
        - asset: assets/fonts/NotoColorEmoji.ttf

25. web/index.html のプリロードブロックの例


<link
  rel="preload"
  href="assets/assets/fonts/AppFont-Regular.ttf"
  as="font"
  type="font/ttf"
  crossorigin
>

<link
  rel="preload"
  href="assets/assets/fonts/AppFont-Bold.ttf"
  as="font"
  type="font/ttf"
  crossorigin
>


<link
  rel="preload"
  href="assets/assets/fonts/NotoColorEmoji.ttf"
  as="font"
  type="font/ttf"
  crossorigin
>


<link
  rel="preload"
  href="assets/assets/images/logo.png"
  as="image"
>

<link
  rel="preload"
  href="assets/assets/images/home_hero.png"
  as="image"
>

メモ

Flutter が宣言済みアセットを assets/ パス以下で配信する一方、アセットパス自体も assets/ で始まるため、Flutter Web のアセット URL には assets/assets/... が含まれることがよくあります。

例:

  • Flutter アセット:assets/images/logo.png
  • Web URL:assets/assets/images/logo.png

26. 起動時の完全な例

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.dart';

class AppStartupPreloader {
  const AppStartupPreloader();

  Future<void> preload(BuildContext context) async {
    await Future.wait([
      _preloadFonts(),
      _preloadImages(context),
      _preloadLocalConfig(),
      _preloadStartupData(),
    ]);
  }

  Future<void> _preloadFonts() {
    return GoogleFonts.pendingFonts([
      GoogleFonts.inter(),
      GoogleFonts.inter(fontWeight: FontWeight.w700),
    ]);
  }

  Future<void> _preloadImages(BuildContext context) {
    return Future.wait([
      precacheImage(
        const AssetImage('assets/images/logo.png'),
        context,
      ),
      precacheImage(
        const AssetImage('assets/images/home_hero.png'),
        context,
      ),
    ]);
  }

  Future<void> _preloadLocalConfig() async {
    await rootBundle.loadString('assets/config/app_config.json');
  }

  Future<void> _preloadStartupData() async {
    // Start your critical first API calls here.
    //
    // Example:
    // await userRepository.getCurrentUser();
    // await dashboardRepository.getDashboard();
  }
}

使用方法:

class StartupGate extends StatefulWidget {
  const StartupGate({super.key});

  @override
  State<StartupGate> createState() => _StartupGateState();
}

class _StartupGateState extends State<StartupGate> {
  Future<void>? _startupFuture;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();

    _startupFuture ??= const AppStartupPreloader().preload(context);
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<void>(
      future: _startupFuture,
      builder: (context, snapshot) {
        if (snapshot.connectionState != ConnectionState.done) {
          return const SplashScreen();
        }

        return const HomeScreen();
      },
    );
  }
}

27. 最後の経験則

プリキャッシュするもの:

  • ユーザーがすぐに目にするもの。
  • ユーザーが次にほぼ確実に目にするもの。
  • 読み込みが遅れると、目に見えるカクつきの原因になるもの。

プリキャッシュしないもの:

  • すべてのもの。
  • 使用されない可能性のある大きなファイル。
  • ファーストビューよりはるか下にあるリモート画像。
  • 重要ではない装飾用の絵文字/フォントアセット。

Flutter Web の場合:

  • ブラウザーでの重要なダウンロードには HTML のプリロードを使用します。
  • Flutter の画像キャッシュには Flutter の precacheImage を使用します。
  • 重要なフォントをバンドルします。
  • 最初の描画で絵文字フォントへのフォールバックが発生しないようにします。
  • 装飾用の絵文字は表示を遅らせます。

参考資料

コメント