通常,Flutter 应用默认性能良好,因此您只需避免常见的陷阱即可获得出色的性能。这些最佳实践建议将帮助您编写出性能最佳的 Flutter 应用。

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

尽量减少昂贵操作

#

有些操作比其他操作更昂贵,这意味着它们消耗更多资源。显然,您希望只在必要时才使用这些操作。您设计和实现应用 UI 的方式可能会对其运行效率产生很大影响。

控制 build() 开销

#

设计 UI 时需要牢记以下几点:

  • 避免在 build() 方法中进行重复且开销大的工作,因为当祖先 widget 重建时,build() 可能会频繁调用。
  • 避免使用带有大型 build() 函数的超大单一 widget。根据封装性以及它们如何变化将它们拆分为不同的 widget。
    • 当在 State 对象上调用 setState() 时,所有子孙 widget 都会重建。因此,将 setState() 调用局限在 UI 实际需要改变的子树部分。如果更改仅限于树的一小部分,请避免在树的较高层级调用 setState()
    • 当重新遇到与上一帧相同的子 widget 实例时,重建所有子孙 widget 的遍历将停止。框架内部大量使用此技术来优化动画,其中动画不影响子子树。请参阅TransitionBuilder 模式和SlideTransition 的源代码,它使用此原理来避免在动画时重建其子孙。(“相同实例”使用 operator == 进行评估,但关于何时避免重写 operator == 的建议,请参阅本页末尾的陷阱部分。)
    • 尽可能在 widget 上使用 const 构造函数,因为它们允许 Flutter 缩短大部分重建工作。为了在可能时自动提醒您使用 const,请启用 flutter_lints 包中推荐的 lint 规则。有关更多信息,请查阅flutter_lints 迁移指南
    • 为了创建可重用的 UI 片段,建议使用 StatelessWidget 而不是函数。

有关更多信息,请查看:


谨慎使用 saveLayer()

#

一些 Flutter 代码使用 saveLayer() 这个开销大的操作来实现 UI 中的各种视觉效果。即使您的代码没有显式调用 saveLayer(),您使用的其他 widget 或包也可能在幕后调用它。也许您的应用调用 saveLayer() 的次数超出了必要;过度调用 saveLayer() 会导致卡顿。

为什么 saveLayer 开销大?

#

调用 saveLayer() 会分配一个离屏缓冲区,并将内容绘制到离屏缓冲区可能会触发渲染目标切换。GPU 希望像消防水带一样全速运行,而渲染目标切换会强制 GPU 暂时重定向该流,然后再将其导回。在移动 GPU 上,这尤其会干扰渲染吞吐量。

何时需要 saveLayer?

#

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

调试 saveLayer 调用

#

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

尽量减少 saveLayer 调用

#

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

  • 如果调用来自您的代码,您可以减少或消除它们吗?例如,您的 UI 可能重叠了两个具有非零透明度的形状。

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

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

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

尽量减少不透明度和裁剪的使用

#

不透明度是另一个开销大的操作,裁剪也是。以下是一些您可能会觉得有用的提示:

  • 仅在必要时使用 Opacity widget。请参阅 Opacity API 页面中的透明图像部分,了解直接将不透明度应用于图像的示例,这比使用 Opacity widget 更快。
  • 与其将简单的形状或文本包装在 Opacity widget 中,通常更快的方法是直接用半透明颜色绘制它们。(但这仅在要绘制的形状中没有重叠部分时才有效。)
  • 要实现图像的淡入效果,可以考虑使用 FadeInImage widget,它使用 GPU 的片段着色器应用渐进不透明度。有关更多信息,请查阅 Opacity 文档。
  • 裁剪不调用 saveLayer()(除非明确通过 Clip.antiAliasWithSaveLayer 请求),因此这些操作不像 Opacity 那样开销大,但裁剪仍然代价高昂,因此请谨慎使用。默认情况下,裁剪是禁用的(Clip.none),因此您需要在必要时显式启用它。
  • 要创建圆角矩形,请考虑使用许多 widget 类提供的 borderRadius 属性,而不是应用裁剪矩形。

谨慎实现网格和列表

#

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

使用惰性加载!

#

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

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

避免固有尺寸

#

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


尽量减少由固有操作引起的布局传递

#

如果您曾进行过大量 Flutter 编程,您可能熟悉在创建 UI 时布局和约束的工作原理。您甚至可能已经记住了 Flutter 的基本布局规则:约束向下传递。尺寸向上返回。父级设置位置。

对于某些 widget,特别是网格和列表,布局过程可能开销大。Flutter 努力只对 widget 执行一次布局传递,但有时需要第二次传递(称为固有传递),这会降低性能。

什么是固有传递?

#

固有传递发生于例如您希望所有单元格都具有最大或最小单元格的大小(或需要轮询所有单元格的类似计算)时。

例如,考虑一个由许多 Card 组成的网格。网格应该有统一大小的单元格,因此布局代码会执行一次传递,从网格的根(在 widget 树中)开始,要求网格中的每个卡片(不仅仅是可见卡片)返回其固有大小——即 widget 在没有约束的情况下首选的大小。有了这些信息,框架会确定一个统一的单元格大小,并第二次访问所有网格单元格,告诉每个卡片要使用的尺寸。

调试固有传递

#

要确定您是否存在过多的固有传递,请在 DevTools 中启用跟踪布局选项(默认禁用),并查看应用的堆栈跟踪以了解执行了多少次布局传递。启用跟踪后,固有时间轴事件会被标记为“$runtimeType intrinsics”。

避免固有传递

#

您有几种方法可以避免固有传递:

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

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


在 16 毫秒内构建和显示帧

#

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

如果您的帧在配置文件模式下总渲染时间远低于 16 毫秒,那么即使存在一些性能陷阱,您也可能不必担心性能问题,但您仍应尽可能快地构建和渲染帧。为什么?

  • 将帧渲染时间降低到 16 毫秒以下可能不会产生视觉差异,但它可以改善电池续航和散热问题。
  • 它可能在您的设备上运行良好,但请考虑您目标设备中性能最低的设备的表现。
  • 随着 120fps 设备变得越来越普及,您会希望在 8 毫秒(总计)内渲染帧,以提供最流畅的体验。

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

陷阱

#

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

此外,您的 IDE 的 Flutter 插件可能也有用。在 Flutter 性能窗口中,启用显示 widget 重建信息复选框。此功能可帮助您检测帧渲染和显示时间是否超过 16 毫秒。在可能的情况下,该插件会提供相关提示的链接。

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

  • 避免使用 Opacity widget,尤其是在动画中避免使用。请改用 AnimatedOpacityFadeInImage。有关更多信息,请查阅不透明度动画的性能注意事项

  • 使用 AnimatedBuilder 时,避免在构建器函数中放置一个子树,该子树构建的 widget 不依赖于动画。该子树会在动画的每个“帧”上重建。相反,只需构建该子树的一部分一次,并将其作为子项传递给 AnimatedBuilder。有关更多信息,请查阅性能优化

  • 避免在动画中进行裁剪。如果可能,请在动画之前预先裁剪图像。

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

  • 避免在 Widget 对象上重写 operator ==。虽然这可能看起来有助于避免不必要的重建,但实际上它会损害性能,因为它会导致 O(N²) 行为。此规则的唯一例外是叶子 widget(没有子项的 widget),在特定情况下,比较 widget 的属性可能比重建 widget 效率显著更高,并且该 widget 很少改变配置。即使在这种情况下,通常也更倾向于依赖缓存 widget,因为即使重写一次 operator ==,也可能导致整体性能下降,因为编译器不再能假定该调用始终是静态的。

资源

#

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