全栈开发知识库

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:完全跳过重建
]);

规则清单:

  1. 能 const 就 const。开启 lint prefer_const_constructors。
  2. 拆小 Widget:把会变化的部分拆成独立的 StatefulWidget/Consumer。用类拆分,而不是用返回 Widget 的方法拆分(方法拆分无法被框架跳过,也拿不到独立的 Element)。
  3. 状态管理精确订阅:context.select、Riverpod 的 ref.watch(provider.select((s) => s.x))、BlocSelector。
  4. builder 模式的 child 参数:AnimatedBuilder(animation: c, child: const Heavy(), builder: (_, child) => Transform.rotate(angle: c.value, child: child)),动画每帧只重建 Transform。
  5. build 中不做耗时工作:不排序大列表、不解析日期、不创建 TextEditingController/ScrollController/AnimationController、不发请求。
  6. 使用 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 预加载

动手练习

  1. 在第 3 章的 feed 流中故意不设置 cacheWidth,用 “Highlight oversized images” 找到问题并修复,对比 DevTools Memory 的差异。
  2. 在页面根部放一个每 100ms setState 的计时器,用 debugPrintRebuildDirtyWidgets 观察,再用 ValueListenableBuilder 优化。
  3. 用 flutter build apk --analyze-size 分析包体积,找出最大的三个部分。
本页由 Markdown 自动渲染 · 共 628 篇笔记