跳到主内容

性能最佳实践

如何确保您的 Flutter 应用具有高性能。

通常情况下,Flutter 应用默认就具备高性能,因此您只需避免常见的陷阱即可获得卓越的性能。这些最佳实践建议将帮助您编写出性能表现尽可能优异的 Flutter 应用。

如何设计一个 Flutter 应用以最高效地渲染场景?特别是,如何确保框架生成的绘制代码尽可能高效?一些渲染和布局操作已知较慢,但并不总能避免。应遵循以下指南,谨慎使用它们。

最小化高开销操作

#

有些操作比其他操作开销更大,这意味着它们消耗更多的资源。显然,您只想在必要时使用这些操作。您设计和实现应用 UI 的方式对运行效率有很大影响。

控制 build() 开销

#

设计 UI 时请牢记以下几点:

  • 避免在 build() 方法中进行重复且昂贵的操作,因为当祖先组件重建时,build() 可能会被频繁调用。
  • 避免使用拥有庞大 build() 函数的单一超大组件。应基于封装原则以及它们的变化频率将它们拆分为不同的组件。
    • 当在一个 State 对象上调用 setState() 时,所有子代组件都会重建。因此,请将 setState() 调用定位到实际需要更改 UI 的子树部分。如果更改仅限于子树的一小部分,请避免在树的高层调用 setState()
    • 当重新遇到与前一帧相同的子组件实例时,重建所有子代的遍历将停止。此技术被框架广泛用于优化动画,即动画不会影响子树的情况。请参阅 TransitionBuilder 模式和 SlideTransition 的源代码,它利用此原理来避免在动画过程中重建其子代。(“相同实例”通过 operator == 进行评估,但请参阅本页末尾的“常见陷阱”部分,获取关于何时避免重写 operator == 的建议。)
    • 尽可能在组件上使用 const 构造函数,因为它们允许 Flutter 跳过大部分重建工作。若要自动提醒您在可能的情况下使用 const,请启用 flutter_lints 包中的推荐 lints。有关更多信息,请查看 flutter_lints 迁移指南
    • 若要创建可复用的 UI 片段,请优先使用 StatelessWidget 而不是函数。

有关更多信息,请查看:


使用 StringBuffer 高效构建字符串

#

当需要从多个部分构建字符串时,特别是在循环内部,使用 + 运算符可能效率低下,因为它在每次拼接时都会创建一个新的 String 对象。更好的方法是使用 StringBuffer,它会收集所有字符串,仅在调用 toString() 时进行一次性拼接。

在 YouTube 新标签页中观看:“StringBuffer (Technique of the Week)”


谨慎使用 saveLayer()

#

一些 Flutter 代码使用 saveLayer()(一种昂贵的操作)来实现 UI 中的各种视觉效果。即使您的代码没有显式调用 saveLayer(),您使用的其他组件或包也可能在后台调用它。也许您的应用调用 saveLayer() 的频率超出了必要程度;过多的 saveLayer() 调用会导致卡顿(jank)。

为什么 saveLayer 昂贵?

#

调用 saveLayer() 会分配一个离屏缓冲区,将内容绘制到该缓冲区可能会触发渲染目标切换。GPU 的工作方式类似于消防水带,而渲染目标切换会强制 GPU 临时重定向流,然后再导回。在移动端 GPU 上,这对渲染吞吐量的干扰尤为严重。

何时需要 saveLayer?

#

在运行时,如果您需要动态显示来自服务器的各种形状(例如),每个形状都带有一定的透明度,且它们可能(或可能不)重叠,那么您几乎必须使用 saveLayer()

调试 saveLayer 调用

#

如何判断您的应用调用 saveLayer() 的频率(无论是直接还是间接)?saveLayer() 方法会在 DevTools 时间轴上触发一个事件;通过在 DevTools 性能视图中勾选 PerformanceOverlayLayer.checkerboardOffscreenLayers 开关,可以了解您的场景何时使用了 saveLayer

最小化 saveLayer 调用

#

您可以避免调用 saveLayer 吗?这可能需要重新思考如何创建视觉效果。

  • 如果这些调用来自您自己的代码,能否减少或消除它们?例如,也许您的 UI 重叠了两个都有非零透明度的形状。

    • 如果它们总是以相同的方式、相同的透明度重叠,您可以预先计算这个重叠的半透明对象的样子并将其缓存,然后使用缓存结果来代替调用 saveLayer()。这适用于任何您可以预先计算的静态形状。
    • 您能否重构绘图逻辑以完全避免重叠?
  • 如果这些调用来自您不拥有的包,请联系包所有者并询问这些调用为何是必要的。它们可以被减少或消除吗?如果不能,您可能需要寻找另一个包,或者编写自己的实现。

其他可能触发 saveLayer() 且开销较大的组件

  • ShaderMask
  • ColorFilter
  • Chip — 如果 disabledColorAlpha != 0xff,可能会触发 saveLayer() 调用。
  • Text — 如果存在 overflowShader,可能会触发 saveLayer() 调用。

最小化不透明度和裁剪的使用

#

不透明度(Opacity)和裁剪(Clipping)也是昂贵的操作。以下是一些有用的建议:

  • 仅在必要时使用 Opacity 组件。请参阅 Opacity API 页面中的 透明图像 部分,了解如何将不透明度直接应用于图像,这比使用 Opacity 组件更快。
  • 与其将简单的形状或文本包裹在 Opacity 组件中,不如直接用半透明颜色绘制它们,这样通常更快。(但这仅在被绘制的形状中没有重叠部分时有效。)
  • 若要实现图像的淡入效果,请考虑使用 FadeInImage 组件,它使用 GPU 的片元着色器应用渐变不透明度。有关更多信息,请查看 Opacity 文档。
  • 裁剪(Clipping)不会调用 saveLayer()(除非显式指定为 Clip.antiAliasWithSaveLayer),因此这些操作不像 Opacity 那样昂贵,但裁剪仍然是有代价的,请谨慎使用。默认情况下,裁剪是禁用的(Clip.none),因此您必须在需要时显式启用它。
  • 若要创建圆角矩形,请考虑使用许多组件类提供的 borderRadius 属性,而不是应用裁剪矩形。

谨慎实现网格和列表

#

您的网格和列表的实现方式可能会导致应用的性能问题。本节描述了创建网格和列表时的一个重要最佳实践,以及如何确定您的应用是否使用了过多的布局传递。

保持懒加载!

#

构建大型网格或列表时,请使用带有回调的懒加载构建器方法。这能确保在启动时只构建屏幕可见的部分。

有关更多信息和示例,请查看:

避免内在测量(Intrinsics)

#

有关内在测量传递如何导致网格和列表问题的信息,请参阅下一节。


最小化由内在(intrinsic)操作引起的布局传递

#

如果您进行过大量 Flutter 开发,您可能熟悉在创建 UI 时布局和约束是如何工作的。您甚至可能记住了 Flutter 的基本布局规则:约束向下传递。尺寸向上传递。父组件决定位置。

对于某些组件(尤其是网格和列表),布局过程可能很昂贵。Flutter 努力对组件执行一次布局传递,但有时需要第二次传递(称为内在测量传递/intrinsic pass),这会降低性能。

什么是内在测量传递?

#

内在测量传递发生在例如您希望所有单元格具有最大或最小单元格的大小(或者需要轮询所有单元格的其他类似计算)时。

例如,考虑一个大型的 Card 网格。网格应该具有统一大小的单元格,因此布局代码执行一次传递,从网格根部(在组件树中)开始,要求网格中的每个卡片(不仅仅是可见卡片)返回其内在尺寸——即假设没有约束时组件偏好的大小。利用这些信息,框架确定统一的单元格大小,并第二次访问所有网格单元,告知每张卡片要使用的大小。

调试内在测量传递

#

要确定是否存在过多的内在测量传递,请在 DevTools 中启用 跟踪布局选项 (Track layouts option)(默认禁用),并查看应用的 堆栈追踪,以了解执行了多少次布局传递。启用跟踪后,内在测量时间轴事件将标记为 '$runtimeType intrinsics'。

避免内在测量传递

#

您有几种选择来避免内在测量传递:

  • 预先将单元格设置为固定大小。
  • 选择特定的单元格作为“锚点”单元格——所有单元格都将相对于该单元格进行调整大小。编写一个自定义 RenderObject,它首先定位子锚点,然后围绕它布局其他子组件。

要深入了解布局的工作原理,请查看 Flutter 架构概览 中的 布局和渲染 部分。


在 16ms 内构建并显示帧

#

由于构建和渲染有两个独立的线程,因此在 60Hz 的显示器上,您有 16ms 用于构建,16ms 用于渲染。如果延迟是一个问题,请在 16ms 或更短的时间内构建并显示一帧。请注意,这意味着在 8ms 或更短的时间内构建,并在 8ms 或更短的时间内渲染,总计 16ms 或更短。

如果在 Profile 模式下您的帧渲染时间总计远低于 16ms,即使存在一些性能陷阱,您可能也不必担心性能,但仍应尽量以最快的速度构建和渲染帧。为什么?

  • 将帧渲染时间降至 16ms 以下可能不会带来明显的视觉差异,但它改善了电池续航和热问题。
  • 它可能在您的设备上运行良好,但请考虑您所针对的最底层设备的性能。
  • 随着 120fps 设备变得越来越普及,您需要将帧渲染时间(总计)控制在 8ms 以下,以提供最流畅的体验。

如果您想知道为什么 60fps 会带来流畅的视觉体验,请观看视频 Why 60fps?

常见陷阱

#

如果您需要调整应用的性能,或者 UI 的流畅度不如预期,DevTools 性能视图 可以为您提供帮助!

此外,您的 IDE 的 Flutter 插件也很有用。在 Flutter Performance 窗口中,启用 Show widget rebuild information 复选框。此功能可帮助您检测帧渲染和显示时间是否超过 16ms。在可能的情况下,插件会提供指向相关提示的链接。

以下行为可能会对应用的性能产生负面影响:

  • 避免使用 Opacity 组件,尤其是在动画中。请改用 AnimatedOpacityFadeInImage。有关更多信息,请查看 不透明度动画的性能考量

  • 使用 AnimatedBuilder 时,避免在 builder 函数中放置与动画无关的组件子树。该子树会在动画的每一帧重新构建。相反,应构建该部分子树一次,并将其作为子组件传递给 AnimatedBuilder。有关更多信息,请查看 性能优化

  • 避免在动画中使用裁剪。如果可能,在动画播放前预先对图像进行裁剪。

  • 避免在大多数子组件在屏幕上不可见时使用带有具体 List 子组件的构造函数(如 Column()ListView()),以避免构建开销。

  • 避免在 Widget 对象上重写 operator ==。虽然看起来可以通过避免不必要的重建来提供帮助,但实际上由于会导致 O(N²) 行为,它会损害性能。此规则的唯一例外是叶子组件(没有子组件的组件),在比较组件属性可能比重建组件更高效且组件配置很少更改的情况下。即使在这种情况下,通常也更倾向于依赖组件缓存,因为即使是一个 operator == 的重写也可能导致全局的性能下降,因为编译器无法再假设该调用总是静态的。

资源

#

有关更多性能信息,请查看以下资源: