姆姆极客分享MU·GEEK·SHARE
// Flutter Rendering Pipeline Build Widget tree Layout size/pos Paint Layer Composite GPU UI Thread Raster Thread frame budget · 16ms (60fps) / 8ms (120fps) jank = pipeline stage > frame budget
移动客户端·

Flutter 性能优化实战指南:从 60fps 到 120fps 的工程路径

Flutter 应用在低端机上常出现 jank,本文从渲染管线、shader 编译、内存、isolate 等角度,给出一套可量化的性能优化方法论。


“Flutter 在中高端机上跑得很丝滑,一到千元机就卡”——这是几乎所有 Flutter 团队都会遇到的抱怨。本文把过去一年我们在一个日活百万级应用上做的性能优化总结成可复用的方法论,希望能帮你少走一些弯路。

一、先建立可量化的基线

没有数字就没有优化。Flutter 自带的性能工具链足够用,关键是要养成“每次发版前跑一遍”的习惯。

1. Flutter DevTools Performance 面板

flutter run --profile --trace-systrace

--profile 模式接近 release 性能,--trace-systrace 把数据导出给 Android systrace / iOS Instruments 联合分析。

关注两个核心指标:

  • UI thread frame time:应稳定在 16ms 以内(60fps),120Hz 设备要 8ms 以内
  • Raster thread frame time:GPU 光栅化时间,超过帧预算就是 shader/图层问题

2. Performance Overlay

MaterialApp(
  showPerformanceOverlay: true,
  // ...
);

开发期常驻一个性能浮层,比事后看 profile 直观得多。蓝色条是 UI 线程,绿色条是 Raster 线程,哪根飙高了看一眼就知道。

二、Shader 编译 jank:第一次启动的“原罪”

Flutter 用 Skia(现已切换到 Impeller)渲染,shader 在第一次绘制时需要编译,造成明显的卡顿。表现是“滚动列表第一次划过去很卡,再划回来就顺了”。

解决方案:SkSL Warmup

import 'package:flutter/material.dart';

// 在 app 启动后、首页渲染前预热 shader
Future<void> warmUpShaders() async {
  final renderer = await RendererBinding.instance;
  // 用一段假数据把首页会用到的渲染路径跑一遍
  await _drawWarmupFrame(renderer);
}

更彻底的方案是开启 Impeller(iOS 已默认开启,Android 14+ 可手动启用):

// android/app/src/main/AndroidManifest.xml
<meta-data
  android:name="io.flutter.embedding.android.EnableImpeller"
  android:value="true" />

Impeller 预编译 shader,从根本上消除了 jank。但它的成熟度还在演进,建议先在内部灰度验证。

三、列表优化:ListView 真的不是有手就行

ListView.builder 默认配置在大多数场景下已经够用,但遇到“每个 item 是一个复杂卡片”的列表,依然要小心。

1. const 构造器:免费但常被忽略

class _ExpensiveCard extends StatelessWidget {
  const _ExpensiveCard({required this.item});
  final Item item;

  @override
  Widget build(BuildContext context) {
    return const Padding(
      padding: EdgeInsets.all(16),  // ← 必须是 const
      child: ...
    );
  }
}

每个 const 都让 Flutter 复用同一个 widget 实例,rebuild 时省掉一整棵对比。一个常量加得越多,性能越稳定。

2. RepaintBoundary:把“局部变化”圈起来

ListView.builder(
  itemBuilder: (context, i) {
    return RepaintBoundary(
      child: ItemCard(item: items[i]),
    );
  },
)

RepaintBoundary 给这个 item 单独开一层 raster 缓存。当 item 内部有动画(如视频缩略图、点赞动效)时,它不会让整列表都重绘。

但不要无脑加——每个 boundary 都会增加一层 layer,加多了反而拖累 GPU。原则:只在确实有独立动画/频繁重绘的 item 上用

3. cacheExtent 的取舍

ListView.builder(
  cacheExtent: 1000,  // 默认 250
  // ...
)

提高 cacheExtent 让列表预渲染更多内容,滑到时不会“突然出现”。代价是 CPU/内存占用上升。视频 feed 这类重 item 建议调小,文字 feed 建议调大。

四、避免 rebuild:从 ChangeNotifier 到 Selector

// 反例:整个 list 都依赖 cart,购物车一变全列表 rebuild
class ItemList extends StatelessWidget {
  Widget build(context) {
    final cart = context.watch<CartModel>();
    return ListView(...);
  }
}

// 正例:只依赖 cart 里和这个 item 相关的部分
class ItemTile extends StatelessWidget {
  Widget build(context) {
    final inCart = context.select<CartModel, bool>(
      (cart) => cart.contains(itemId),
    );
    return ...;
  }
}

select 让 widget 只在“自己关心的那部分状态”变化时才 rebuild。在 Provider/Riverpod 里这是最容易被忽略的免费优化。

五、Isolate:把重计算搬出 UI 线程

import 'dart:isolate';

Future<String> parseLargeJson(String raw) {
  return Isolate.run(() {
    // 在独立 isolate 中执行,不阻塞 UI
    return jsonDecode(raw).toString();
  });
}

Isolate.run 是 Dart 3.0+ 引入的简化 API,比 compute() 更灵活。任何超过 16ms 的同步计算(JSON 解析、图片处理、加密)都该丢给 isolate。

六、图片:占用内存的“沉默杀手”

// 反例:原图 4000x3000,但只显示 200x150
Image.network('https://.../huge.jpg')

// 正例:让服务器出缩略图,或本地缓存时压缩
CachedNetworkImage(
  imageUrl: 'https://.../huge.jpg?w=400',
  memCacheWidth: 400,  // 解码后只缓存 400px 宽的位图
  placeholder: (_, __) => ShimmerBox(),
)

memCacheWidth/memCacheHeight 是 Image widget 的隐藏宝藏,能让内存占用直接降一个数量级。

七、量化结果

经过上述优化,我们在红米 Note 11(中端机)上的指标:

指标 优化前 优化后
冷启动 1.4s 0.8s
列表滑动 90th frame time 32ms 11ms
内存峰值 410MB 230MB
滚动 jank 比例 14% 1.2%

结语

Flutter 性能优化的核心不是“调几个参数”,而是建立一套可量化的工程流程——每次发版前看 profile、每次优化后看数字、每个改动都有回归基线。一旦把这套流程跑起来,性能问题就不再是“线上救火”,而是“工程内控”。