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. 04为 Flutter 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. 网络图片

将 precacheImage 与 NetworkImage 配合使用。

@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,请使用 didChangeDependencies,而不是 initState。


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. 适合预加载表情符号的情况

适合的情况:

  • 聊天应用
  • 表情回应
  • 评论
  • 社交界面
  • 表情符号选择器
  • 大量使用表情符号的引导流程
  • 品牌大量使用表情符号

不适合的情况:

  • 欢迎屏幕上的一个装饰性表情符号
  • 按钮上的随机表情符号
  • 应用深处很少使用的表情符号

规则:

  • 如果表情符号只是装饰,应延迟显示,或避免在首次绘制时显示。
  • 如果表情符号是产品的核心内容,应采用成熟的表情符号渲染策略。

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 Web 资源 URL 通常包含 assets/assets/...,因为 Flutter 会在 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 precacheImage 缓存 Flutter 图片。
  • 打包关键字体。
  • 避免首次绘制时回退到表情符号字体。
  • 延迟显示装饰性表情符号。

参考资料

评论

支持 Markdown