性能最佳实践

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

您如何设计 Flutter 应用程序以最有效地渲染您的场景?特别是,您如何确保框架生成的绘图代码尽可能高效?已知某些渲染和布局操作很慢,但并不总是可以避免的。应该深思熟虑地使用它们,遵循以下指导。

最大程度减少昂贵的操作

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

控制 build() 成本

在设计 UI 时需要记住以下事项

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

有关更多信息,请查看


谨慎使用 saveLayer()

部分 Flutter 代码使用 saveLayer()(一项开销较大的操作)在 UI 中实现各种视觉效果。即使你的代码没有明确调用 saveLayer(),其他你使用的组件或软件包也可能会在后台调用它。也许你的应用调用 saveLayer() 的次数超出了必要;对 saveLayer() 的过度调用会导致卡顿。

为什么 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 窗口小部件。请参阅 Opacity API 页面中的 透明图像 部分,了解将不透明度直接应用到图像的示例,这比使用 Opacity 窗口小部件更快。
  • 通常,与其将简单的形状或文本包装在 Opacity 窗口小部件中,不如直接用半透明颜色绘制它们更快。(但这仅适用于待绘制形状中没有重叠部分时。)
  • 若要在图像中实现淡入,请考虑使用 FadeInImage 窗口小部件,它使用 GPU 的片段着色器应用渐变不透明度。有关更多信息,请查看 Opacity 文档。
  • 剪切不会调用 saveLayer()(除非使用 Clip.antiAliasWithSaveLayer 显式请求),因此这些操作不像 Opacity 那么昂贵,但剪切仍然很昂贵,因此请谨慎使用。默认情况下,剪切处于禁用状态(Clip.none),因此您必须在需要时显式启用它。
  • 要创建带有圆角的矩形,请考虑使用许多窗口小部件类提供的 borderRadius 属性,而不是应用剪切矩形。

深思熟虑地实现网格和列表

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

偷懒!

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

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

避免内在

有关内在传递如何可能导致网格和列表出现问题的信息,请参阅下一部分。


最小化由内在操作导致的布局传递

如果您已经做了很多 Flutter 编程,那么您可能熟悉在创建 UI 时布局和约束如何工作。您甚至可能已经记住了 Flutter 的基本布局规则:约束下降。尺寸上升。父级设置位置。

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

什么是内在传递?

例如,当您希望所有单元格的大小与最大或最小单元格的大小(或需要轮询所有单元格的类似计算)相同时,就会发生内在传递。

例如,考虑一个包含大量 Card 的大网格。网格应具有大小一致的单元格,因此布局代码会执行一次遍历,从网格的根部(在小部件树中)开始,要求网格中的每个卡片(不仅仅是可见的卡片)返回其固有大小,即小部件在没有约束条件下的首选大小。利用这些信息,框架会确定一个统一的单元格大小,并再次访问所有网格单元格,告诉每个卡片应使用什么大小。

调试固有遍历

若要确定是否存在过多的固有遍历,请在 DevTools 中启用跟踪布局选项(默认情况下处于禁用状态),并查看应用的堆栈跟踪以了解执行了多少次布局遍历。启用跟踪后,固有时间线事件将标记为“$runtimeType intrinsics”。

避免固有遍历

有几种选项可用于避免固有遍历

  • 预先将单元格设置为固定大小。
  • 选择一个特定的单元格作为“锚点”单元格,所有单元格都将相对于此单元格调整大小。编写一个自定义渲染对象,该对象首先定位子锚点,然后在其周围布局其他子对象。

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


在 16ms 内构建并显示帧

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

如果你的帧在配置文件模式下总共在 16ms 内渲染,即使存在一些性能陷阱,你可能也不必担心性能,但你仍应尽可能快地构建并渲染一个帧。为什么?

  • 将帧渲染时间降低到 16ms 以下可能不会产生视觉差异,但它可以提高电池续航时间并解决散热问题。
  • 它可能在你的设备上运行良好,但请考虑你所针对的最低设备的性能。
  • 随着 120fps 设备的普及,你将需要在 8ms(总计)内渲染帧,以提供最流畅的体验。

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

陷阱

如果你需要调整应用程序的性能,或者用户界面不如预期流畅,DevTools Performance 视图 可以提供帮助!

此外,IDE 的 Flutter 插件可能有用。在 Flutter Performance 窗口中,启用显示小部件重建信息复选框。此功能可帮助你检测何时以超过 16 毫秒的速度渲染和显示帧。在可能的情况下,插件会提供指向相关提示的链接。

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

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

  • 使用 AnimatedBuilder 时,避免在生成函数中放置不依赖于动画的生成小部件的子树。此子树将在动画的每次滴答声中重建。相反,一次生成子树的那一部分,并将其作为子项传递给 AnimatedBuilder。有关更多信息,请查看 性能优化

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

  • 如果屏幕上看不到大多数子项,请避免使用带有具体 List 子项的构造函数(例如 Column()ListView())以避免构建成本。

  • 避免重写 Widget 对象上的 operator ==。虽然它看起来似乎可以通过避免不必要的重建来提供帮助,但在实践中,它会损害性能,因为它会导致 O(N²) 行为。此规则的唯一例外是叶级小组件(没有子组件的小组件),在比较小组件的属性很可能比重建小组件更有效率并且小组件很少会更改配置的特定情况下。即使在这种情况下,通常也最好依赖缓存小组件,因为即使重写一次 operator == 也会导致全面性能下降,因为编译器无法再假定该调用始终是静态的。

资源

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