关于 flutter-pre-caching
面向 Flutter 和 Flutter Web 开发者的预缓存指南,说明如何优先加载近期可见的字体、图片、配置与数据,以减少首屏延迟和卡顿。
主要功能
- 01字体与图片预加载
- 02启动配置和数据预热
- 03Lottie 与 Rive 资源加载
- 04Flutter Web 浏览器预加载
- 05按近期可见内容控制缓存
适用场景
- 01减少首屏字体切换与图片闪烁
- 02预加载应用启动所需资源
- 03加快首屏动画显示
- 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 图片。 - 打包关键字体。
- 避免首次绘制时回退到表情符号字体。
- 延迟显示装饰性表情符号。