flutter-performance
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseFlutter Performance
Flutter 性能
Hold a steady 60 fps (<=16 ms/frame; <=8 ms on 120 Hz) on a floor device (low-end Android / older iPhone). Performance is a property you measure in profile mode, not one you assert. It comes from rebuilding less, painting cheaply, and keeping both the UI thread and the raster thread free.
在低端真机(低端 Android / 较旧的 iPhone)上稳定保持 60 fps(每帧 <=16 ms;120 Hz 时 <=8 ms)。性能是一个需要在 profile mode 中测量的属性,而不是仅凭断言得出的结论。它来自更少重建、更廉价的绘制,以及让 UI thread 和 raster thread 保持空闲。
Non-negotiable rules
不可妥协的规则
- everything legal. A
constsubtree is skipped on rebuild — the framework short-circuits it by identity. Keepconston as an error; a non-const literal that could be const is a lint failure, not a preference.prefer_const_constructors - Shrink the rebuild scope to the smallest changing widget. Never for a whole state object when one field changed — watch
ref.watch(provider). A HUD counter tick must never rebuild a heavy sibling. Rebuild scope is the single biggest lever on build-thread cost.provider.select((s) => s.field) - Lazy everything long. /
ListView.builder/ slivers for variable or unbounded content.GridView.builderbuilds every off-screen row up front — refuse it for anything not tiny and fixed.ListView(children: items.map(...).toList()) - No expensive work in . No
build(), sort, regex,jsonDecodemath, file/network, or large allocation —DateTimemay run every frame. Compute in the Notifier/ViewModel once and cache the result in immutable state.build() - Heavy CPU off the UI isolate. Parse large payloads, process images, crunch numbers in or a spawned
compute(). Blocking the UI thread is guaranteed jank; the raster thread cannot save you.Isolate - Size image decode to the display slot. /
cacheWidthorcacheHeight, correct asset resolutions,ResizeImagefor above-the-fold art. Decoding a 4000 px source into a 100 px box spikes memory and OOMs cheap phones.precacheImage - Prefer the cheapest widget that does the job. /
ColoredBoxoverDecoratedBox,Containerfor spacing,SizedBox/FadeTransitionover theAnimatedOpacitywidget in hot paths. AvoidOpacity/ClipPathinside scrolling lists — they force an offscreen buffer on the raster thread.saveLayer - around costly, independently-repainting subtrees (an animation, a chart, a live indicator) so its repaint does not re-raster its neighbours. Do not sprinkle boundaries everywhere — each is a compositor layer that costs memory.
RepaintBoundary - Pass expensive children through .
child:/AnimatedBuilder/ValueListenableBuilderrebuild only theStreamBuilder; hand the unchanging subtree in viabuilderso it is built once, not per frame.child: - Dispose to avoid leaks. s, gesture recognizers, stream subscriptions, timers,
AnimationControllers, caches — released inTextEditingController(widgets) ordispose()(providers).ref.onDisposescoped session state so it does not survive the screen.autoDispose - Measure in PROFILE mode on a real floor device with DevTools. Debug timings are meaningless (JIT, asserts). Watch the raster thread as well as the UI thread — /blur/clip cost shows there, not in Dart. Never ship a frame-budget number that was asserted, not measured.
saveLayer
- 尽可能为所有合法项使用 。
const子树在重建时会被跳过——框架会根据 identity 直接短路处理。保持const开启并按 error 处理;本可使用 const 的非 const 字面量属于 lint 失败,而不是可选项。prefer_const_constructors - 将重建范围缩小到最小发生变化的 Widget。 当一个字段发生变化时,绝不要对整个 state object 使用 ——应使用
ref.watch(provider)进行监听。HUD 计数器的一次 tick 绝不能重建重型同级 Widget。重建范围是降低 build thread 成本最有效的单一杠杆。provider.select((s) => s.field) - 所有长列表都必须惰性构建。 对于可变或无界内容,使用 /
ListView.builder/ slivers。GridView.builder会预先构建每一行,包括屏幕外的行——除内容极小且固定外,一律不要使用这种写法。ListView(children: items.map(...).toList()) - 不要在 中执行高开销工作。 不要在
build()中进行build()、排序、正则表达式匹配、jsonDecode计算、文件/网络访问或大规模内存分配——DateTime可能每帧都会运行。在 Notifier/ViewModel 中计算一次,并将结果缓存在 immutable state 中。build() - 将高 CPU 负载移出 UI isolate。 在 或独立创建的
compute()中解析大型 payload、处理图片或执行密集数值计算。阻塞 UI thread 必然造成卡顿;raster thread 无法替你解决这些问题。Isolate - 按照显示区域尺寸解码图片。 使用 /
cacheWidth或cacheHeight,提供正确的资源分辨率,并对首屏可见图片使用ResizeImage。将 4000 px 的源图解码到 100 px 的显示区域会引发内存峰值,并导致廉价手机 OOM。precacheImage - 选择能完成任务的最低成本 Widget。 使用 /
ColoredBox而不是DecoratedBox,使用Container处理间距,在热点路径中使用SizedBox/FadeTransition而不是AnimatedOpacityWidget。避免在滚动列表中使用Opacity/ClipPath——它们会迫使 raster thread 创建离屏缓冲区。saveLayer - 在开销高且可独立重绘的子树周围使用 (例如动画、图表或实时指示器),避免其重绘导致相邻节点重新栅格化。不要到处添加 boundary——每个 boundary 都是一个会消耗内存的 compositor layer。
RepaintBoundary - 通过 传递高开销子节点。
child:/AnimatedBuilder/ValueListenableBuilder只会重建StreamBuilder;通过builder传入不变的子树,使其只构建一次,而不是每帧重新构建。child: - 释放资源以避免内存泄漏。 、手势识别器、stream subscription、timer、
AnimationController、缓存等,必须在TextEditingController(Widget)或dispose()(Provider)中释放。对 session state 使用ref.onDispose,避免其在页面退出后继续存在。autoDispose - 使用 DevTools,在真实低端设备上以 PROFILE mode 进行测量。 Debug 模式的计时数据没有意义(JIT、asserts)。同时观察 raster thread 和 UI thread——/blur/clip 的开销会显示在这里,而不是 Dart 中。绝不能发布仅靠断言、未经测量的帧预算数字。
saveLayer
Narrow the rebuild scope
缩小重建范围
Watch the single field that changes, not the whole state object:
dart
// Rebuilds only when the count changes, not on every OrderState mutation.
class OrderBadge extends ConsumerWidget {
const OrderBadge({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(orderProvider.select((s) => s.items.length));
return Text('$count');
}
}.select==state-management-riverpodfamilyautoDispose只监听发生变化的单个字段,而不是整个 state object:
dart
// 仅在 count 变化时重建,而不是每次 OrderState 发生变化时都重建。
class OrderBadge extends ConsumerWidget {
const OrderBadge({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(orderProvider.select((s) => s.items.length));
return Text('$count');
}
}每次 publish 时, 都会执行 selector,但只有选中值的 发生变化时才会触发重建,因此其输入必须是具有真实相等语义的 immutable value。有关 watch/read/listen 的区分以及 + ,请参阅 。
.select==familyautoDisposestate-management-riverpodKeep expensive children out of the animation loop
将高开销子节点排除在动画循环之外
dart
// The card is built once; only the transform recomputes each tick.
AnimatedBuilder(
animation: _controller,
child: const ProductCard(),
builder: (_, child) =>
Transform.scale(scale: _controller.value, child: child),
);dart
// 卡片只构建一次;每个 tick 仅重新计算 transform。
AnimatedBuilder(
animation: _controller,
child: const ProductCard(),
builder: (_, child) =>
Transform.scale(scale: _controller.value, child: child),
);Lazy lists and sized images
惰性列表与按尺寸解码图片
dart
// Lazy — only visible rows are built.
ListView.builder(
itemCount: items.length,
itemBuilder: (context, i) => ItemTile(item: items[i]),
);
// Decode to the slot, not the source resolution.
Image.asset('assets/hero.png', cacheWidth: 240);dart
// 惰性构建——只构建可见行。
ListView.builder(
itemCount: items.length,
itemBuilder: (context, i) => ItemTile(item: items[i]),
);
// 按显示区域尺寸解码,而不是使用源图分辨率。
Image.asset('assets/hero.png', cacheWidth: 240);Heavy work off the UI isolate
将重负载工作移出 UI isolate
dart
// parseItems is a top-level or static function (isolate entry-point rules).
final items = await compute(parseItems, jsonString);dart
// parseItems 是顶层函数或 static 函数(isolate 入口函数要求)。
final items = await compute(parseItems, jsonString);Custom painting
自定义绘制
For a , the discipline is zero-allocation , a that is one value comparison over an immutable scene value, and a (the ) instead of in a ticker. That treatment lives in ; the one performance-critical rule to carry here: never allocate // inside — hold them as painter fields and mutate cheap properties.
CustomPainterpaint()shouldRepaintrepaint:ListenableAnimationControllersetStatecustom-canvas-and-gesturesPaintPathGradientpaint()对于 ,应遵循以下规范: 必须零分配; 只对 immutable scene value 执行一次值比较;使用 (),而不是在 ticker 中调用 。更详细的说明位于 ;这里需要保留的性能关键规则是:绝不要在 内部分配 //——应将其保存为 painter 字段,并修改成本较低的属性。
CustomPainterpaint()shouldRepaintrepaint:ListenableAnimationControllersetStatecustom-canvas-and-gesturespaint()PaintPathGradientProfiling workflow
性能分析流程
- on a physical floor device.
flutter run --profile - DevTools -> Performance; record while reproducing the jank.
- For each over-budget frame, check whether the time is in Build, Layout, or Raster.
- Build-heavy -> narrow rebuild scope with , add
.select, move work out ofconst.build() - Raster-heavy -> reduce /clips/
Opacity, add asaveLayer, simplify shaders.RepaintBoundary
- Build-heavy -> narrow rebuild scope with
- Use "Track widget rebuilds" to find widgets rebuilding too often.
- Fix one bottleneck, re-measure. Never optimize without a before/after measurement.
- 在实体低端设备上执行 。
flutter run --profile - 打开 DevTools -> Performance;在复现卡顿时录制性能数据。
- 对于每个超出预算的帧,检查耗时位于 Build、Layout 还是 Raster。
- Build 开销过高 -> 使用 缩小重建范围,添加
.select,并将工作移出const。build() - Raster 开销过高 -> 减少 /clip/
Opacity的使用,添加saveLayer,简化 shader。RepaintBoundary
- Build 开销过高 -> 使用
- 使用“Track widget rebuilds”功能查找重建过于频繁的 Widget。
- 每次只修复一个瓶颈,然后重新测量。没有优化前后的测量数据,绝不进行优化。
Anti-patterns
反模式
- Judging performance in debug mode — JIT and asserts make timings fiction. Profile mode, real device, always.
- Premature optimization with no profile data — adds complexity, hides nothing; measure first.
- high in the tree rebuilding a whole screen for a tiny change — scope the state down.
setState() - for a whole object in a leaf when one field changed — use
ref.watch(provider)..select - for long/unbounded content — builds every off-screen row.
ListView(children: [...].toList()) - /sort/
jsonDecodemath/regex inDateTime— it may run every frame; precompute in the ViewModel.build() - widget for fades in scrolling lists — forces
Opacity; usesaveLayeror fade via color.FadeTransition - Decoding full-resolution images into small widgets — memory spikes and OOM on cheap phones.
- Parsing large payloads / heavy loops on the UI isolate — frozen frames; use .
compute - on everything — layer-memory bloat; use it surgically around independently-repainting subtrees.
RepaintBoundary - /
AnimatedBuilderrebuilding an expensive child instead of passing it viaStreamBuilder.child: - Rebuilding on every keystroke without debounce, or unbounded caches/listeners that leak.
- Allocating /
PaintinsidePath— per-frame garbage; hold them as fields.paint()
- 在 debug mode 中判断性能——JIT 和 asserts 会让计时结果失真。应始终使用 profile mode 和真实设备。
- 在没有 profile 数据的情况下过早优化——只会增加复杂度,并不能解决任何问题;应先测量。
- 在树的较高层级调用 ,导致微小变化重建整个屏幕——应缩小 state 作用域。
setState() - 在叶子节点对整个对象使用 ,即使只有一个字段发生变化——应使用
ref.watch(provider)。.select - 对长内容或无界内容使用 ——会构建每一个屏幕外行。
ListView(children: [...].toList()) - 在 中执行
build()/排序/jsonDecode计算/正则表达式匹配——它可能每帧运行;应预先在 ViewModel 中完成计算。DateTime - 在滚动列表中使用 Widget 实现淡入淡出——会强制生成
Opacity;应使用saveLayer或通过颜色实现淡出。FadeTransition - 将全分辨率图片解码到小型 Widget 中——会造成内存峰值,并导致廉价手机 OOM。
- 在 UI isolate 中解析大型 payload 或执行高开销循环——会冻结帧;应使用 。
compute - 对所有内容都使用 ——会造成 layer 内存膨胀;应只在可独立重绘的子树周围精准使用。
RepaintBoundary - 让 /
AnimatedBuilder重建高开销 child,而不是通过StreamBuilder传入。child: - 每次按键都触发重建却没有 debounce,或使用无界缓存/listener 导致泄漏。
- 在 内分配
paint()/Paint——会产生逐帧垃圾;应将其保存为字段。Path
Definition of done
完成标准
- applied everywhere legal;
constclean.prefer_const_constructors - Rebuilds scoped with ; a tick in one widget never rebuilds a heavy sibling.
.select - Long/variable lists are lazy (/slivers).
.builder - No I/O, parsing, or heavy compute in ; big work runs via
build()/compute.Isolate - Images sized to display (/
cacheWidth); above-the-fold precached.ResizeImage - Cheapest suitable widgets/effects chosen; used surgically; expensive children passed via
RepaintBoundary.child: - Any allocates nothing in
CustomPainter;paint()is one value compare.shouldRepaint - Controllers, recognizers, subscriptions, timers disposed; scoped session state d; no leaks.
autoDispose - Measured in profile mode on a floor device via DevTools — UI and raster threads under budget; no frames over 16 ms; claim backed by a recording.
- 所有合法项均已应用 ;
const检查无错误。prefer_const_constructors - 使用 限定重建范围;一个 Widget 的 tick 不会重建重型同级 Widget。
.select - 长列表或可变列表采用惰性构建(/slivers)。
.builder - 中没有 I/O、解析或高开销计算;大型任务通过
build()/compute执行。Isolate - 图片按照显示尺寸解码(/
cacheWidth);首屏可见图片已预先缓存。ResizeImage - 选择成本最低且适用的 Widget/效果;精准使用 ;高开销子节点通过
RepaintBoundary传入。child: - 任何 在
CustomPainter中都不产生分配;paint()仅执行一次值比较。shouldRepaint - Controller、手势识别器、subscription、timer 均已释放;session state 已使用 ;不存在泄漏。
autoDispose - 已通过 DevTools 在低端真机上以 profile mode 测量——UI 和 raster thread 均未超出预算;没有超过 16 ms 的帧;性能结论有录制数据作为依据。
Related skills
相关 skills
- — the watch/read/listen split,
state-management-riverpod,.select+family, immutable state that makesautoDisposecorrect..select - — small
widget-compositionWidget classes overconstmethods, dispose discipline, cheapest-widget choices._buildX - — zero-allocation
custom-canvas-and-gestures,paint()as one value compare,shouldRepaintrepaint:.Listenable - — clock-injected pure core so heavy compute is testable off the UI isolate.
testing-strategy
- ——watch/read/listen 的区分、
state-management-riverpod、.select+family,以及确保autoDispose正确工作的 immutable state。.select - ——使用小型
widget-compositionWidget 类代替const方法、资源释放规范,以及最低成本 Widget 的选择。_buildX - ——零分配
custom-canvas-and-gestures、只执行一次值比较的paint(),以及shouldRepaintrepaint:。Listenable - ——使用注入时钟的纯核心,使高开销计算可以在 UI isolate 之外进行测试。
testing-strategy