11 性能问题与优化
结论先行:Flutter 在 Release(AOT)模式下,绝大多数业务页面的性能可以达到或接近原生。性能问题通常来自写法不当(过度重建、大图、长列表写法错误、主线程重计算)、PlatformView 滥用,以及混合开发中的引擎创建。下面按“如何测量 → 常见问题 → 解决”展开。
11.1 先测量,不要猜
必须在 Profile 模式、真机上测
flutter run --profile # 性能接近 release,可连接 DevTools
Debug 模式是 JIT + 大量断言,比 Release 慢数倍,Debug 下卡不代表线上卡。模拟器的 GPU 与真机完全不同,也不能作为依据。选一台低端机作为基准设备。
DevTools
flutter pub global activate devtools # IDE 一般已内置
# flutter run 后终端会打印 DevTools 地址;或在 Android Studio 的 Flutter 面板打开
| 面板 | 用途 |
|---|---|
| Performance | 帧柱状图,UI 与 Raster 各自耗时;Timeline 事件;开启 “Track widget builds” 查看每帧重建了哪些 Widget |
| CPU Profiler | Dart 方法耗时火焰图 |
| Memory | Dart 堆、快照对比、分配追踪,查内存泄漏 |
| Network | dio/http 请求 |
| Widget Inspector | 布局调试、“Highlight repaints”、“Highlight oversized images” |
| App Size | 分析包体积构成 |
判断瓶颈在哪个线程
帧柱状图中:
- UI 线程超时(Dart 侧):build 太重、layout 复杂、同步计算、JSON 解析 → 优化 Dart 代码。
- Raster 线程超时(GPU 侧):
saveLayer、模糊、阴影、裁剪、大图纹理上传、着色器编译 → 优化绘制。
辅助开关:
import 'package:flutter/rendering.dart';
debugRepaintRainbowEnabled = true; // 每次重绘换颜色,找到不该重绘的区域
debugPrintRebuildDirtyWidgets = true; // 打印每帧重建的 Widget
debugProfileBuildsEnabled = true; // Timeline 中显示每个 build
MaterialApp(showPerformanceOverlay: true) 在设备上显示两条帧耗时曲线(上:Raster,下:UI)。
11.2 重建(Build)优化
问题:setState 放在了页面根部,一个小变化导致整页重建。
// 差:倒计时每秒 setState,整个页面(包括列表)每秒重建
class _PageState extends State<Page> {
int _seconds = 60;
// ... Timer 中 setState(() => _seconds--)
@override
Widget build(BuildContext context) => Column(children: [
Text('$_seconds'),
const Expanded(child: HugeList()),
]);
}
// 好:把变化的部分下沉为独立 Widget,或用 ValueListenableBuilder 缩小范围
final _seconds = ValueNotifier(60);
Column(children: [
ValueListenableBuilder<int>(
valueListenable: _seconds,
builder: (_, v, __) => Text('$v'),
),
const Expanded(child: HugeList()), // const:完全跳过重建
]);
规则清单:
- 能
const就const。开启 lintprefer_const_constructors。 - 拆小 Widget:把会变化的部分拆成独立的 StatefulWidget/Consumer。用类拆分,而不是用返回 Widget 的方法拆分(方法拆分无法被框架跳过,也拿不到独立的 Element)。
- 状态管理精确订阅:
context.select、Riverpod 的ref.watch(provider.select((s) => s.x))、BlocSelector。 builder模式的child参数:AnimatedBuilder(animation: c, child: const Heavy(), builder: (_, child) => Transform.rotate(angle: c.value, child: child)),动画每帧只重建 Transform。build中不做耗时工作:不排序大列表、不解析日期、不创建TextEditingController/ScrollController/AnimationController、不发请求。- 使用
MediaQuery.sizeOf(context)等细分方法,避免键盘弹出触发全页面重建。
11.3 布局与绘制优化
| 高开销操作 | 说明 | 替代方案 |
|---|---|---|
Opacity 包裹复杂子树 |
触发 saveLayer,离屏渲染 |
图片用 color.withValues(alpha:);动画用 FadeTransition/AnimatedOpacity(会走优化路径) |
ClipRRect/ClipPath |
裁剪需要额外 GPU 工作,滚动中大量裁剪开销大 | 图片圆角用 BoxDecoration(image:, borderRadius:);或者让设计用预处理的圆角图 |
BackdropFilter(毛玻璃) |
读取背景并模糊,非常昂贵 | 尽量少用,限制区域,不放在列表 item 中;低端机降级 |
BoxShadow 大模糊半径 |
模糊计算 | 减小 blurRadius,或使用 9-patch 阴影图 |
IntrinsicHeight/IntrinsicWidth |
额外的布局遍历(打破单遍布局) | 重新设计布局,或固定尺寸 |
ShaderMask、ColorFiltered |
saveLayer | 谨慎使用 |
RepaintBoundary:把频繁重绘的区域(动画、进度条、视频封面上的倒计时)与静态区域隔离。
RepaintBoundary(child: LottieAnimation());
不要到处滥用:每个 RepaintBoundary 都是一个独立 Layer,会占用额外的 GPU 内存。用 “Highlight repaints” 确认确实存在不必要的重绘再加。
11.4 长列表
ListView.builder(
itemCount: items.length,
itemExtent: 96, // 固定高度 → 跳过子项布局测量,快速跳转
// 或 prototypeItem: const FeedItem.placeholder(), 让框架按原型计算高度
cacheExtent: 500, // 预加载视口外的区域(逻辑像素)
itemBuilder: (context, i) => FeedItem(key: ValueKey(items[i].id), data: items[i]),
);
- 必须用
.builder/SliverList.builder懒加载;不要shrinkWrap: true;不要Column+SingleChildScrollView放上百项。 - item 中的图片必须限制解码尺寸(见 11.5)。
- 避免在 item 中放 PlatformView(每个都是原生 View,滚动时开销巨大)。
- 不需要保持 item 状态时,
addAutomaticKeepAlives: false可以减少内存。 - 分页加载,数据量巨大时只保留窗口内数据。
11.5 图片(内存问题的头号来源)
问题:一张 4000×3000 的图片,即使显示在 100×100 的区域,默认也会按原尺寸解码,占用 4000×3000×4 ≈ 46MB 内存。
// 1. 按显示尺寸解码(最重要)
Image.network(
url,
width: 120, height: 68,
cacheWidth: (120 * MediaQuery.devicePixelRatioOf(context)).round(), // 只解码到需要的尺寸
fit: BoxFit.cover,
);
// cached_network_image 对应 memCacheWidth / memCacheHeight
CachedNetworkImage(imageUrl: url, memCacheWidth: 360);
// 2. 限制全局图片缓存(默认 1000 张 / 100MB)
PaintingBinding.instance.imageCache
..maximumSize = 200
..maximumSizeBytes = 80 << 20; // 80MB
// 3. 服务端按尺寸下发缩略图(CDN 参数),从源头减少数据量
DevTools Widget Inspector 的 “Highlight oversized images” 会把解码尺寸远大于显示尺寸的图片倒转显示,方便定位。
其他:
- 优先使用 WebP/AVIF;大量 GIF 动图非常耗内存和 CPU。
- 页面退出后
imageCache里的图仍然存在,内存紧张时调用imageCache.clear()或依赖didHaveMemoryPressure自动清理。
11.6 主线程计算与 ANR
Flutter 3.29 起 Dart 运行在平台主线程(第 6 章),Dart 侧长时间同步计算会造成 Android ANR,而不仅是掉帧。
// 大 JSON 解析、加解密、图片处理、复杂排序 → Isolate
final list = await Isolate.run(() => parseFeed(body));
// dio 的大响应可以配置在后台 Isolate 解析
dio.transformer = BackgroundTransformer(); // dio 5 默认已是 BackgroundTransformer(大响应走 compute)
经验阈值:主线程上单个同步任务超过约 8~16ms 就会影响帧率,应考虑拆分或放入 Isolate。
原生插件同样要注意:onMethodCall 在主线程执行,里面的 IO、数据库、解码必须切到后台线程,否则同时卡住原生和 Flutter。
11.7 首帧卡顿(Jank)与着色器
- Skia 时代的“首次动画卡顿”是运行时着色器编译导致的。Impeller 预编译着色器,iOS 和 Android(Vulkan)上基本消除。
- 如果 Android 设备回退到 OpenGL ES,或者你使用的是较旧的 Flutter 版本/Flutter OH 版本,首次出现的复杂效果仍可能卡顿。
- 自定义着色器(
FragmentProgram)在首次加载时有编译开销,提前预加载。
11.8 启动速度
| 阶段 | 优化 |
|---|---|
| 原生启动 | 与原生 App 一样:精简 Application.onCreate,延迟非必要 SDK 初始化 |
| 引擎初始化 | Add-to-App 场景提前预热 FlutterEngine(第 7 章);多引擎用 FlutterEngineGroup |
Dart main() |
不要在 runApp 前 await 大量初始化。先 runApp 显示骨架,再并行初始化 |
| 首页首帧 | 首屏数据缓存(先显示本地缓存再刷新);首屏只构建可见部分;首屏图片限制尺寸 |
| 启动页 | 使用原生 SplashScreen,Flutter 第一帧渲染后自动移除,避免白屏 |
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await initCritical(); // 只初始化首屏必需的(如读取登录态)
runApp(const App());
unawaited(initNonCritical()); // 埋点、推送等延后
}
测量启动时间:
flutter run --trace-startup --profile # 输出 build/start_up_info.json,包含引擎初始化到首帧的耗时
11.9 包体积
Flutter 会给 App 增加固定体积(引擎 + ICU 数据 + 框架代码),Android 单 ABI 约 +4~6MB(压缩后),业务代码增长与原生相当。
# Android:按 ABI 拆分(上架 Google Play 用 AAB 自动拆分)
flutter build apk --release --split-per-abi
flutter build appbundle --release
# 混淆 Dart 符号并把调试信息分离出来(崩溃栈需要用这些符号文件还原)
flutter build apk --release --obfuscate --split-debug-info=build/symbols
# 分析体积
flutter build apk --release --analyze-size --target-platform android-arm64
其他手段:
- 去掉未使用的资源、字体;中文字体只打包子集。
- Material Icons 默认已做 tree shaking(只保留用到的图标)。
- 大型低频模块使用 deferred components(
deferred as+ Android 动态特性模块),按需下载。 - 国内应用市场一般只需
arm64-v8a(如需兼容老机型再加armeabi-v7a)。
11.10 内存泄漏
常见泄漏点:
| 泄漏来源 | 修复 |
|---|---|
AnimationController、TextEditingController、ScrollController、FocusNode、TapGestureRecognizer 未 dispose |
在 dispose() 中释放 |
StreamSubscription、Timer 未取消 |
dispose() 中 cancel() |
ChangeNotifier.addListener 未移除 |
removeListener |
| 闭包被全局单例持有(回调注册到全局 EventBus),捕获了 State | 页面销毁时反注册 |
| 原生侧持有 Activity/FlutterEngine | 插件 onDetachedFromActivity 中释放引用 |
| Add-to-App 每次新建 FlutterEngine 未销毁 | 复用或调用 engine.destroy() |
工具:DevTools Memory → 两次快照 Diff;开发阶段集成 leak_tracker(Flutter 官方的泄漏检测,可在 widget 测试中启用 LeakTesting)。
11.11 PlatformView / 混合开发的性能
- Android Hybrid Composition 模式下,Flutter 的光栅化需要与平台线程同步,低端机滚动掉帧明显;默认的 TLHC 好很多;3.44 的 HCPP 是目前最好的方案(需满足 API/硬件条件,手动开启)。
- 能用
Texture就不用 PlatformView(视频、相机、原生特效预览)。 - WebView:页面以 WebView 为主时,考虑直接用原生页面承载,而不是 Flutter 中嵌 WebView。
- 同屏 PlatformView 越少越好;不要在列表 item 中使用。
- Channel 通信不要高频(每帧)调用;大数据用
Uint8List或 FFI。
11.12 三端性能对比与注意点
| 平台 | 渲染后端 | 特点与注意 |
|---|---|---|
| Android | Impeller + Vulkan(API 29+),不支持时回退 OpenGL ES | 设备碎片化最严重,低端机 GPU 弱;必须在低端机测试;部分机型驱动问题 |
| iOS | Impeller + Metal | 最稳定,性能通常最好;内存限制严格 |
| 鸿蒙 | Flutter OH:OpenGL ES,新版本支持 Impeller(Vulkan) | 整体可用,版本落后于官方,部分优化(如 HCPP、最新 Impeller 改进)要等社区同步;PlatformView、键盘、字体等边界功能需要逐个验证;以真机 release 包为准 |
11.13 性能问题排查速查表
| 现象 | 优先排查 |
|---|---|
| 列表滑动掉帧 | 是否用了 builder;item 图片是否限制解码尺寸;是否有 Opacity/Clip/阴影;item build 是否过重;是否有 PlatformView |
| 页面打开慢 | 是否在 initState 同步做了重活;是否等待网络再渲染(应先显示骨架);混合开发是否每次新建引擎 |
| 动画卡 | 是否整页 setState;Raster 线程是否超时(模糊/saveLayer);是否在 Debug 模式测试 |
| 内存持续上涨 | 图片缓存;未 dispose 的 Controller/Subscription;多引擎 |
| ANR | Dart 同步计算、原生插件在主线程做 IO |
| 首次某个效果卡一下 | OpenGL ES 回退设备上的着色器编译;自定义 shader 预加载 |
动手练习
- 在第 3 章的 feed 流中故意不设置
cacheWidth,用 “Highlight oversized images” 找到问题并修复,对比 DevTools Memory 的差异。 - 在页面根部放一个每 100ms
setState的计时器,用debugPrintRebuildDirtyWidgets观察,再用ValueListenableBuilder优化。 - 用
flutter build apk --analyze-size分析包体积,找出最大的三个部分。