flutter-performance

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Flutter 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

不可妥协的规则

  1. const
    everything legal.
    A
    const
    subtree is skipped on rebuild — the framework short-circuits it by identity. Keep
    prefer_const_constructors
    on as an error; a non-const literal that could be const is a lint failure, not a preference.
  2. Shrink the rebuild scope to the smallest changing widget. Never
    ref.watch(provider)
    for a whole state object when one field changed — watch
    provider.select((s) => s.field)
    . A HUD counter tick must never rebuild a heavy sibling. Rebuild scope is the single biggest lever on build-thread cost.
  3. Lazy everything long.
    ListView.builder
    /
    GridView.builder
    / slivers for variable or unbounded content.
    ListView(children: items.map(...).toList())
    builds every off-screen row up front — refuse it for anything not tiny and fixed.
  4. No expensive work in
    build()
    .
    No
    jsonDecode
    , sort, regex,
    DateTime
    math, file/network, or large allocation —
    build()
    may run every frame. Compute in the Notifier/ViewModel once and cache the result in immutable state.
  5. Heavy CPU off the UI isolate. Parse large payloads, process images, crunch numbers in
    compute()
    or a spawned
    Isolate
    . Blocking the UI thread is guaranteed jank; the raster thread cannot save you.
  6. Size image decode to the display slot.
    cacheWidth
    /
    cacheHeight
    or
    ResizeImage
    , correct asset resolutions,
    precacheImage
    for above-the-fold art. Decoding a 4000 px source into a 100 px box spikes memory and OOMs cheap phones.
  7. Prefer the cheapest widget that does the job.
    ColoredBox
    /
    DecoratedBox
    over
    Container
    ,
    SizedBox
    for spacing,
    FadeTransition
    /
    AnimatedOpacity
    over the
    Opacity
    widget in hot paths. Avoid
    ClipPath
    /
    saveLayer
    inside scrolling lists — they force an offscreen buffer on the raster thread.
  8. RepaintBoundary
    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.
  9. Pass expensive children through
    child:
    .
    AnimatedBuilder
    /
    ValueListenableBuilder
    /
    StreamBuilder
    rebuild only the
    builder
    ; hand the unchanging subtree in via
    child:
    so it is built once, not per frame.
  10. Dispose to avoid leaks.
    AnimationController
    s, gesture recognizers, stream subscriptions, timers,
    TextEditingController
    s, caches — released in
    dispose()
    (widgets) or
    ref.onDispose
    (providers).
    autoDispose
    scoped session state so it does not survive the screen.
  11. 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 —
    saveLayer
    /blur/clip cost shows there, not in Dart. Never ship a frame-budget number that was asserted, not measured.
  1. 尽可能为所有合法项使用
    const
    。
    const
    子树在重建时会被跳过——框架会根据 identity 直接短路处理。保持
    prefer_const_constructors
    开启并按 error 处理;本可使用 const 的非 const 字面量属于 lint 失败,而不是可选项。
  2. 将重建范围缩小到最小发生变化的 Widget。 当一个字段发生变化时,绝不要对整个 state object 使用
    ref.watch(provider)
    ——应使用
    provider.select((s) => s.field)
    进行监听。HUD 计数器的一次 tick 绝不能重建重型同级 Widget。重建范围是降低 build thread 成本最有效的单一杠杆。
  3. 所有长列表都必须惰性构建。 对于可变或无界内容,使用
    ListView.builder
    /
    GridView.builder
    / slivers。
    ListView(children: items.map(...).toList())
    会预先构建每一行,包括屏幕外的行——除内容极小且固定外,一律不要使用这种写法。
  4. 不要在
    build()
    中执行高开销工作。
    不要在
    build()
    中进行
    jsonDecode
    、排序、正则表达式匹配、
    DateTime
    计算、文件/网络访问或大规模内存分配——
    build()
    可能每帧都会运行。在 Notifier/ViewModel 中计算一次,并将结果缓存在 immutable state 中。
  5. 将高 CPU 负载移出 UI isolate。 在
    compute()
    或独立创建的
    Isolate
    中解析大型 payload、处理图片或执行密集数值计算。阻塞 UI thread 必然造成卡顿;raster thread 无法替你解决这些问题。
  6. 按照显示区域尺寸解码图片。 使用
    cacheWidth
    /
    cacheHeight
    或
    ResizeImage
    ,提供正确的资源分辨率,并对首屏可见图片使用
    precacheImage
    。将 4000 px 的源图解码到 100 px 的显示区域会引发内存峰值,并导致廉价手机 OOM。
  7. 选择能完成任务的最低成本 Widget。 使用
    ColoredBox
    /
    DecoratedBox
    而不是
    Container
    ,使用
    SizedBox
    处理间距,在热点路径中使用
    FadeTransition
    /
    AnimatedOpacity
    而不是
    Opacity
    Widget。避免在滚动列表中使用
    ClipPath
    /
    saveLayer
    ——它们会迫使 raster thread 创建离屏缓冲区。
  8. 在开销高且可独立重绘的子树周围使用
    RepaintBoundary
    (例如动画、图表或实时指示器),避免其重绘导致相邻节点重新栅格化。不要到处添加 boundary——每个 boundary 都是一个会消耗内存的 compositor layer。
  9. 通过
    child:
    传递高开销子节点。
    AnimatedBuilder
    /
    ValueListenableBuilder
    /
    StreamBuilder
    只会重建
    builder
    ;通过
    child:
    传入不变的子树,使其只构建一次,而不是每帧重新构建。
  10. 释放资源以避免内存泄漏。
    AnimationController
    、手势识别器、stream subscription、timer、
    TextEditingController
    、缓存等,必须在
    dispose()
    (Widget)或
    ref.onDispose
    (Provider)中释放。对 session state 使用
    autoDispose
    ,避免其在页面退出后继续存在。
  11. 使用 DevTools,在真实低端设备上以 PROFILE mode 进行测量。 Debug 模式的计时数据没有意义(JIT、asserts)。同时观察 raster thread 和 UI thread——
    saveLayer
    /blur/clip 的开销会显示在这里,而不是 Dart 中。绝不能发布仅靠断言、未经测量的帧预算数字。

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
runs the selector every publish but rebuilds only when the selected value's
==
changes, so its input must be an immutable value with real equality. See
state-management-riverpod
for the watch/read/listen split and
family
+
autoDispose
.
只监听发生变化的单个字段,而不是整个 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 时,
.select
都会执行 selector,但只有选中值的
==
发生变化时才会触发重建,因此其输入必须是具有真实相等语义的 immutable value。有关 watch/read/listen 的区分以及
family
+
autoDispose
,请参阅
state-management-riverpod
。

Keep 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
CustomPainter
, the discipline is zero-allocation
paint()
, a
shouldRepaint
that is one value comparison over an immutable scene value, and a
repaint:
Listenable
(the
AnimationController
) instead of
setState
in a ticker. That treatment lives in
custom-canvas-and-gestures
; the one performance-critical rule to carry here: never allocate
Paint
/
Path
/
Gradient
inside
paint()
— hold them as painter fields and mutate cheap properties.
对于
CustomPainter
,应遵循以下规范:
paint()
必须零分配;
shouldRepaint
只对 immutable scene value 执行一次值比较;使用
repaint:
Listenable
(
AnimationController
),而不是在 ticker 中调用
setState
。更详细的说明位于
custom-canvas-and-gestures
;这里需要保留的性能关键规则是:绝不要在
paint()
内部分配
Paint
/
Path
/
Gradient
——应将其保存为 painter 字段,并修改成本较低的属性。

Profiling workflow

性能分析流程

  1. flutter run --profile
    on a physical floor device.
  2. DevTools -> Performance; record while reproducing the jank.
  3. For each over-budget frame, check whether the time is in Build, Layout, or Raster.
    • Build-heavy -> narrow rebuild scope with
      .select
      , add
      const
      , move work out of
      build()
      .
    • Raster-heavy -> reduce
      Opacity
      /clips/
      saveLayer
      , add a
      RepaintBoundary
      , simplify shaders.
  4. Use "Track widget rebuilds" to find widgets rebuilding too often.
  5. Fix one bottleneck, re-measure. Never optimize without a before/after measurement.
  1. 在实体低端设备上执行
    flutter run --profile
    。
  2. 打开 DevTools -> Performance;在复现卡顿时录制性能数据。
  3. 对于每个超出预算的帧,检查耗时位于 Build、Layout 还是 Raster。
    • Build 开销过高 -> 使用
      .select
      缩小重建范围,添加
      const
      ,并将工作移出
      build()
      。
    • Raster 开销过高 -> 减少
      Opacity
      /clip/
      saveLayer
      的使用,添加
      RepaintBoundary
      ,简化 shader。
  4. 使用“Track widget rebuilds”功能查找重建过于频繁的 Widget。
  5. 每次只修复一个瓶颈,然后重新测量。没有优化前后的测量数据,绝不进行优化。

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.
  • setState()
    high in the tree
    rebuilding a whole screen for a tiny change — scope the state down.
  • ref.watch(provider)
    for a whole object in a leaf
    when one field changed — use
    .select
    .
  • ListView(children: [...].toList())
    for long/unbounded content
    — builds every off-screen row.
  • jsonDecode
    /sort/
    DateTime
    math/regex in
    build()
    — it may run every frame; precompute in the ViewModel.
  • Opacity
    widget for fades in scrolling lists
    — forces
    saveLayer
    ; use
    FadeTransition
    or fade via color.
  • 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
    .
  • RepaintBoundary
    on everything
    — layer-memory bloat; use it surgically around independently-repainting subtrees.
  • AnimatedBuilder
    /
    StreamBuilder
    rebuilding an expensive child
    instead of passing it via
    child:
    .
  • Rebuilding on every keystroke without debounce, or unbounded caches/listeners that leak.
  • Allocating
    Paint
    /
    Path
    inside
    paint()
    — per-frame garbage; hold them as fields.
  • 在 debug mode 中判断性能——JIT 和 asserts 会让计时结果失真。应始终使用 profile mode 和真实设备。
  • 在没有 profile 数据的情况下过早优化——只会增加复杂度,并不能解决任何问题;应先测量。
  • 在树的较高层级调用
    setState()
    ,导致微小变化重建整个屏幕——应缩小 state 作用域。
  • 在叶子节点对整个对象使用
    ref.watch(provider)
    ,即使只有一个字段发生变化——应使用
    .select
    。
  • 对长内容或无界内容使用
    ListView(children: [...].toList())
    ——会构建每一个屏幕外行。
  • 在
    build()
    中执行
    jsonDecode
    /排序/
    DateTime
    计算/正则表达式匹配
    ——它可能每帧运行;应预先在 ViewModel 中完成计算。
  • 在滚动列表中使用
    Opacity
    Widget 实现淡入淡出
    ——会强制生成
    saveLayer
    ;应使用
    FadeTransition
    或通过颜色实现淡出。
  • 将全分辨率图片解码到小型 Widget 中——会造成内存峰值,并导致廉价手机 OOM。
  • 在 UI isolate 中解析大型 payload 或执行高开销循环——会冻结帧;应使用
    compute
    。
  • 对所有内容都使用
    RepaintBoundary
    ——会造成 layer 内存膨胀;应只在可独立重绘的子树周围精准使用。
  • 让
    AnimatedBuilder
    /
    StreamBuilder
    重建高开销 child
    ,而不是通过
    child:
    传入。
  • 每次按键都触发重建却没有 debounce,或使用无界缓存/listener 导致泄漏。
  • 在
    paint()
    内分配
    Paint
    /
    Path
    ——会产生逐帧垃圾;应将其保存为字段。

Definition of done

完成标准

  • const
    applied everywhere legal;
    prefer_const_constructors
    clean.
  • Rebuilds scoped with
    .select
    ; a tick in one widget never rebuilds a heavy sibling.
  • Long/variable lists are lazy (
    .builder
    /slivers).
  • No I/O, parsing, or heavy compute in
    build()
    ; big work runs via
    compute
    /
    Isolate
    .
  • Images sized to display (
    cacheWidth
    /
    ResizeImage
    ); above-the-fold precached.
  • Cheapest suitable widgets/effects chosen;
    RepaintBoundary
    used surgically; expensive children passed via
    child:
    .
  • Any
    CustomPainter
    allocates nothing in
    paint()
    ;
    shouldRepaint
    is one value compare.
  • Controllers, recognizers, subscriptions, timers disposed; scoped session state
    autoDispose
    d; no leaks.
  • 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
    检查无错误。
  • 使用
    .select
    限定重建范围;一个 Widget 的 tick 不会重建重型同级 Widget。
  • 长列表或可变列表采用惰性构建(
    .builder
    /slivers)。
  • build()
    中没有 I/O、解析或高开销计算;大型任务通过
    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

  • state-management-riverpod
    — the watch/read/listen split,
    .select
    ,
    family
    +
    autoDispose
    , immutable state that makes
    .select
    correct.
  • widget-composition
    — small
    const
    Widget classes over
    _buildX
    methods, dispose discipline, cheapest-widget choices.
  • custom-canvas-and-gestures
    — zero-allocation
    paint()
    ,
    shouldRepaint
    as one value compare,
    repaint:
    Listenable
    .
  • testing-strategy
    — clock-injected pure core so heavy compute is testable off the UI isolate.
  • state-management-riverpod
    ——watch/read/listen 的区分、
    .select
    、
    family
    +
    autoDispose
    ,以及确保
    .select
    正确工作的 immutable state。
  • widget-composition
    ——使用小型
    const
    Widget 类代替
    _buildX
    方法、资源释放规范,以及最低成本 Widget 的选择。
  • custom-canvas-and-gestures
    ——零分配
    paint()
    、只执行一次值比较的
    shouldRepaint
    ,以及
    repaint:
    Listenable
    。
  • testing-strategy
    ——使用注入时钟的纯核心,使高开销计算可以在 UI isolate 之外进行测试。

References

参考资料