目录

Flutter 性能优化最佳实践

在竞争激烈的移动领域,流畅且响应迅速的用户体验不仅是一种优势,它还是一种必要条件。然而,Flutter 开发者经常面临着创建高性能应用程序的挑战,与潜在的延迟、卡顿和响应缓慢作斗争。有一些最佳实践可以进一步提高性能。

使用最新版本的 Flutter

最新版本的 Flutter 始终包含最新的性能优化。因此,保持你的 Flutter SDK 为最新版本非常重要。

避免使用调试模式

Flutter 中的调试模式可用于调试代码,但它也会显著影响性能。因此,在生产环境中避免使用调试模式非常重要。

使用合适的 Widget

Flutter 提供了各种各样的组件,可用于构建用户界面。选择使用合适的组件非常重要,因为一些组件比其他组件性能更好。例如,ListView 的性能优于 GridView

避免不必要的重建

当对象的 state 更改时,Flutter 会自动重建使用该对象的组件。但是,如果组件不必要地重建,它可能会影响性能。因此,避免不必要的重建非常重要。

使用 profile 模式运行进行性能分析

Flutter 的 profile 模式几乎与 release 模式相同地编译并启动应用程序,但仅添加了足够的功能以允许调试性能问题。例如,分析模式会向分析工具提供跟踪信息。

注意:DevTools 无法连接到以分析模式运行的 Flutter Web 应用程序。使用 Chrome DevTools 为 Web 应用程序生成时间线事件

使用 DevTools

Flutter DevTools 是一种可用来调试和分析 Flutter 应用程序的工具。DevTools 可用于实时查看应用程序的性能,并识别潜在的性能瓶颈。

在真机上测试应用程序

在真机上测试应用程序对于真实了解其性能非常重要。模拟器和模拟工具对于开发很有用,但它们可能无法准确反映应用程序在真机上的性能。

监视应用程序的性能

发布应用程序后,监视其性能非常重要。这将帮助你识别可能出现的任何性能问题,并采取措施来解决它们。

最小化高消耗操作

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

控制 build() 成本

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

  • 避免在 build() 方法中进行重复且耗时的操作,因为 build() 可能会在祖先组件重建时频繁调用。
  • 避免使用 build() 函数过大的单一大型组件。根据封装以及它们的更改方式将它们拆分成不同的组件:
  • setState()State 对象上调用时,所有子组件都会重建。因此,将 setState() 调用定位到实际上需要更改 UI 的子树部分。如果更改包含在树的一个小部分中,请避免在树中较高位置调用 setState()
  • 当遇到与前一帧相同的子组件实例时,重建所有子组件的遍历停止。在框架内,此技术大量用于优化动画,动画不会影响子树。请参阅 TransitionBuilder 模式和 SlideTransition 源代码,它使用此原则避免在动画时重建其子组件。(“相同实例”使用 operator == 进行评估,但请参阅此页面结尾处的陷阱部分,了解何时避免覆写 operator == 的建议)。
  • 尽可能在组件上使用 const 构造函数,因为它们可以让 Flutter 绕过大部分重建工作。要自动收到在可能的情况下使用 const 的提醒,请在 flutter_lints 包中启用推荐 lints。
  • 要创建可重复使用的 UI 部分,建议使用 StatelessWidget 而不是函数。

有关更多信息,请查看:

  • StatefulWidget API 文档的中的性能注意事项
  • Widgets vs helper methods,官方 Flutter YouTube 频道的一个视频,解释了为什么组件(特别是具有 const 构造函数的组件)的性能比函数更好。

慎重使用 saveLayer()

一些 Flutter 代码使用 saveLayer(),这是一个昂贵的操作,在 UI 中实现各种视觉效果。即使你的代码没有明确调用 saveLayer(),你使用的其他组件或包也可能会在后台调用它。也许你的应用程序调用的 saveLayer() 过于频繁;对 saveLayer() 的过多调用可能会导致卡顿。

为什么 saveLayer 很昂贵?

调用 saveLayer() 会分配一个离屏缓冲区,而将内容绘制到离屏缓冲区可能会触发渲染目标切换。GPU 希望像消防软管一样运行,而渲染目标切换会迫使 GPU 暂时重定向该流,然后再次导向回它。在移动 GPU 上,这对渲染吞吐量的影响尤为严重。

何时需要 saveLayer?

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

调试对 saveLayer 的调用

你怎么能判断你的应用程序直接或间接调用 saveLayer() 的频率?saveLayer() 方法在 DevTools 时间表 上触发一个事件;通过在 DevTools Performance 视图 中查看 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 明确请求),因此这些操作不像不透明度那么耗费资源,但剪切仍然很昂贵,因此请小心使用。默认情况下,将禁用剪切(Clip.none),因此在需要时你必须显式启用它。
  • 要创建具有圆角的矩形,请考虑使用许多组件类提供的 borderRadius 属性,而不是应用剪切矩形。

慎重实现 Grids 和 Lists

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

勤于利用延迟加载!

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

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

避免使用内部属性

有关内部属性可能如何引起网格和列表问题的信息,请参阅下一节。

最小化由内部操作引起的布局传递

如果你完成了许多 Flutter 编程,你可能熟悉在创建 UI 时 布局和约束如何工作。你甚至可能记住了 Flutter 的基本布局规则:约束向下。尺寸向上。父级设置位置

对于某些组件,特别是网格和列表,布局过程可能会很耗费资源。Flutter努力只进行一次小部件的布局传递,但有时,需要第二次传递(称为内部传递),这可能会降低性能。

什么是内部传递?

当你希望所有单元格都具有最大或最小单元格的大小(或需要轮询所有单元格的类似计算)时,就会发生内部传递。

例如,考虑一个大的卡片网格。网格应该有统一大小的单元格,所以布局代码从网格的根(在小部件树中)开始进行传递,要求网格中的每个卡片(不仅仅是可见的卡片)返回其内部大小——小部件在没有约束的情况下的首选大小。有了这个信息,框架确定一个统一的单元格大小,并再次访问所有网格单元格,告诉每个卡片应使用的大小。

调试内部传递

要确定你是否有过多的内部传递,启用 DevTools 中的Track layouts option(默认关闭),并查看应用程序的堆栈跟踪以了解执行了多少次布局传递。一旦启用追踪,内部时间线事件将被标记为$runtimeType intrinsics

避免内部传递

你可以通过以下几种方式来避免内部传递:

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

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

在 16 毫秒内构建并显示帧

由于构建和渲染有独立的两个线程,你在 60Hz 屏幕上有 16 毫秒用于构建,16 毫秒用于渲染。如果延迟是个问题,则在 16 毫秒或更短的时间内构建和显示一个帧。请注意,这意味着在 8 毫秒或更短的时间内构建,在 8 毫秒或更短的时间内呈现,总计 16 毫秒或更短。

如果你的帧在 profile 模式下的总渲染时间远低于 16 毫秒,即使存在一些性能缺陷,你可能也不必担心性能问题,但你仍然应该尽量快速地构建和渲染一个帧。为什么?

  • 将帧渲染时间降至 16 毫秒以下可能不会带来视觉差异,但可以 延长电池续航时间并解决 热量问题。
  • 它可能在你的设备上运行良好,但请考虑你定位的最低设备的性能。
  • 随着 120fps 设备的普及,为了提供最流畅的体验,你需要在不到 8 毫秒(总计)的时间内渲染帧。

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

陷阱

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

此外,适用于你的 IDE 的 Flutter 插件可能也很有用。在 Flutter Performance 窗口中,启用 Track Widget Builds 复选框。此功能可帮助你检测什么时候以超过 16 毫秒的时间渲染和显示帧。在可能的情况下,该插件会提供一个与相关提示的链接。

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

  • 避免使用 Opacity 组件,尤其不要在动画中使用它。改用 AnimatedOpacityFadeInImage。有关更多信息,请查看 不透明度动画的性能注意事项
  • 使用 AnimatedBuilder 时,避免在构建器函数中放置一个构建器,该构建器构建不依赖于动画的组件。该组件的子树对于动画的每一次滴答都将被重建。相反,一次构建该组件的这部分,并将其作为子级传递给 AnimatedBuilder。有关更多信息,请查看 性能优化
  • 避免在动画中剪切。如果可能,在对其进行动画处理之前预剪切图像。
  • 避免使用带有具体 List 子级的构造函数(例如,Column()ListView()),如果大多数子级在屏幕上不可见,则避免构建成本。
  • 避免在 Widget 对象上重写 operator ==。虽然它似乎可以通过避免不必要的重建来提供帮助,但在实践中它会损害性能,因为它会导致 O(N²) 行为。此规则的唯一例外是叶级组件(没有子级的组件),在特定情况下,比较组件的属性可能比重建组件并罕见的情况更有可能更改配置。即使在这些情况下,最好依赖缓存组件,因为即使一个 operator == 的重写也可能导致整个性能下降,因为编译器不再可以假定调用始终是静态的。

性能 FAQ