性能最佳实践
总的来说,Flutter 应用默认情况下是高性能的,因此您只需要避免常见的陷阱就能获得出色的性能。这些最佳实践建议将帮助您编写出尽可能高性能的 Flutter 应用。
您如何设计一个 Flutter 应用以最高效地渲染您的场景?特别是,您如何确保框架生成的绘制代码尽可能高效?一些渲染和布局操作已知速度较慢,但并非总是可以避免。应谨慎使用它们,并遵循以下指南。
最小化昂贵操作
#某些操作比其他操作更昂贵,这意味着它们消耗更多的资源。显然,您只希望在必要时使用这些操作。您的应用 UI 的设计和实现方式会对运行效率产生很大影响。
控制 build() 的成本
#在设计 UI 时,请牢记以下几点:
- 避免在
build()
方法中进行重复且耗时的操作,因为当父级小部件重建时,build()
可能会被频繁调用。 - 避免创建具有大型
build()
函数的单个过大的 widget。根据封装性以及它们的变化方式,将它们拆分成不同的 widget。- 当在
State
对象上调用setState()
时,所有子孙 widget 都会重建。因此,将setState()
调用限制在 UI 实际需要更改的子树部分。如果更改仅限于子树的一小部分,请避免在高层级调用setState()
。 - 当重新遇到与上一帧相同的子 widget 实例时,重建所有子孙的遍历会停止。此技术在框架内大量用于优化动画,当动画不影响子孙子树时。请参阅
TransitionBuilder
模式和SlideTransition
的源代码,它利用此原理在动画时避免重建其子孙。(“相同的实例”通过operator ==
进行评估,但关于何时避免重写operator ==
的建议,请参阅页面末尾的陷阱部分。) - 尽可能多地在 widget 上使用
const
构造函数,因为它们允许 Flutter 缩短大部分重建工作。要自动获得使用const
的提醒,请启用flutter_lints
包中推荐的 lint。有关更多信息,请查看flutter_lints
迁移指南。 - 要创建可重用的 UI 片段,请优先使用
StatelessWidget
而不是函数。
- 当在
有关更多信息,请参阅:
- 性能注意事项,这是
StatefulWidget
API 文档的一部分。 - Widgets vs helper methods,来自官方 Flutter YouTube 频道的视频,解释了为什么 widget(特别是带有
const
构造函数的 widget)比函数更具性能。
使用 StringBuffer 来高效地构建字符串
#当需要将多个部分合并成一个字符串时,尤其是在循环内部,使用 +
运算符可能会效率低下,因为它会在每次连接时创建一个新的 String
对象。更好的方法是使用 StringBuffer
,它会收集所有字符串,并在调用 toString()
时只连接一次。
谨慎使用 saveLayer()
#一些 Flutter 代码使用 saveLayer()
(一种昂贵的操作)来实现 UI 中的各种视觉效果。即使您的代码没有显式调用 saveLayer()
,您使用的其他 widget 或包也可能在后台调用它。也许您的应用调用 saveLayer()
的次数超过了必要,过多的 saveLayer()
调用会导致卡顿。
为什么 saveLayer() 昂贵?
#调用 saveLayer()
会分配一个屏幕外缓冲区,并将内容绘制到屏幕外缓冲区可能会触发渲染目标切换。GPU 就像一个消防水龙带,而渲染目标切换会迫使 GPU 暂时重定向该流,然后再将其导回。在移动 GPU 上,这对渲染吞吐量尤其具有破坏性。
什么时候需要 saveLayer()?
#在运行时,如果您需要动态显示来自服务器(例如)的各种形状,每个形状都有一些透明度,这些形状可能会(也可能不会)重叠,那么您几乎必须使用 saveLayer()
。
调试 saveLayer() 调用
#您如何知道您的应用直接或间接调用 saveLayer()
的频率?saveLayer()
方法会在 DevTools 时间轴上触发一个事件;通过检查 DevTools Performance 视图中的 PerformanceOverlayLayer.checkerboardOffscreenLayers
开关,了解您的场景何时使用 saveLayer
。
最小化 saveLayer() 调用
#您能否避免调用 saveLayer
?这可能需要重新考虑您创建视觉效果的方式。
如果调用来自您的代码,您能否减少或消除它们?例如,您的 UI 可能重叠了两个形状,每个形状都有非零透明度。
- 如果它们总是以相同的方式、相同的重叠量、相同的透明度重叠,您可以预先计算出这个重叠的半透明对象的外观,将其缓存起来,然后使用它而不是调用
saveLayer()
。这适用于任何您可以预先计算的静态形状。 - 能否重构您的绘制逻辑以完全避免重叠?
- 如果它们总是以相同的方式、相同的重叠量、相同的透明度重叠,您可以预先计算出这个重叠的半透明对象的外观,将其缓存起来,然后使用它而不是调用
如果调用来自您不拥有的包,请联系包所有者并询问为什么这些调用是必要的。能否减少或消除它们?如果不能,您可能需要寻找另一个包,或编写自己的包。
其他可能触发 saveLayer()
且可能昂贵的 widget:
ShaderMask
ColorFilter
Chip
—如果disabledColorAlpha != 0xff
,可能会触发对saveLayer()
的调用。Text
—如果存在overflowShader
,可能会触发对saveLayer()
的调用。
最小化 opacity 和 clipping 的使用
#Opacity 和 clipping 也是昂贵的操作。以下是一些您可能会觉得有用的技巧:
- 仅在必要时使用
Opacity
widget。请参阅Opacity
API 页面上的 透明图像 部分,其中有一个直接将不透明度应用于图像的示例,这比使用Opacity
widget 更快。 - 与其将简单的形状或文本包装在
Opacity
widget 中,不如通常使用半透明颜色绘制它们更快。(尽管这只适用于要绘制的形状中没有重叠部分的情况。) - 要实现图像渐隐效果,请考虑使用
FadeInImage
widget,它使用 GPU 的片段着色器应用渐进式不透明度。有关更多信息,请查看Opacity
文档。 - Clipping 不会调用
saveLayer()
(除非明确使用Clip.antiAliasWithSaveLayer
请求),因此这些操作不像Opacity
那样昂贵,但 clipping 仍然很昂贵,所以请谨慎使用。默认情况下,clipping 被禁用(Clip.none
),因此您必须在需要时显式启用它。 - 要创建带有圆角的矩形,请不要应用 clipping 矩形,而是考虑使用许多 widget 类提供的
borderRadius
属性。
谨慎实现 grids 和 lists
#您的 grids 和 lists 的实现方式可能会导致应用的性能问题。本节介绍创建 grids 和 lists 时的一项重要最佳实践,以及如何确定您的应用是否使用了过多的布局传递。
保持惰性!
#构建大型 grid 或 list 时,请使用惰性构建器方法和回调。这确保在启动时只构建屏幕上可见的部分。
有关更多信息和示例,请参阅:
- 处理长列表
- 创建一次加载一页的
ListView
,AbdulRahman AlHamali 的社区文章。 Listview.builder
API
避免 intrinsic 操作
#有关 intrinsic 传递可能导致 grids 和 lists 出现问题的详细信息,请参阅下一节。
最小化由 intrinsic 操作引起的布局传递
#如果您已经进行了大量的 Flutter 编程,您可能熟悉 创建 UI 时布局和约束的工作原理。您甚至可能已经记住了 Flutter 的基本布局规则:约束向下传递,尺寸向上返回,父级设置位置。
对于某些 widget,尤其是 grids 和 lists,布局过程可能会很昂贵。Flutter 努力只对 widget 进行一次布局传递,但有时需要第二次传递(称为intrinsic传递),这会降低性能。
什么是 intrinsic 传递?
#当您希望所有单元格具有最大或最小单元格的尺寸(或类似需要轮询所有单元格的计算)时,就会发生 intrinsic 传递。
例如,考虑一个包含大量 Card
的 grid。一个 grid 应该具有统一大小的单元格,因此布局代码会执行一次传递,从 grid 的根(在 widget 树中)开始,询问 grid 中的每个卡片(不只是可见的卡片)返回其intrinsic大小——即 widget 在没有约束条件下的首选大小。有了这些信息,框架会确定一个统一的单元格大小,并第二次遍历所有 grid 单元格,告知每个卡片要使用的大小。
调试 intrinsic 传递
#要确定您是否有过多的 intrinsic 传递,请在 DevTools 中启用Track layouts 选项(默认禁用),并查看应用的 堆栈跟踪,了解执行了多少次布局传递。启用跟踪后,intrinsic 时间轴事件会标记为 '$runtimeType intrinsics'。
避免 intrinsic 传递
#您有几种选择可以避免 intrinsic 传递:
- 预先将单元格设置为固定大小。
- 选择一个特定的单元格作为“锚点”单元格——所有单元格的大小都将相对于此单元格。编写一个自定义
RenderObject
,它首先定位子锚点,然后围绕它布局其他子项。
要更深入地了解布局的工作原理,请参阅 Flutter 架构概述中的 布局和渲染部分。
在 16 毫秒内构建和显示帧
#由于构建和渲染有两个独立的线程,因此在 60Hz 显示器上,您有 16 毫秒用于构建,16 毫秒用于渲染。如果延迟是一个问题,请在 16 毫秒或更短的时间内构建和显示一帧。请注意,这意味着在 8 毫秒或更短时间内构建,在 8 毫秒或更短时间内渲染,总共不超过 16 毫秒。
如果在profile 模式下,您的帧渲染时间远低于 16 毫秒,即使存在一些性能陷阱,您可能也不必担心性能,但您仍应努力尽快构建和渲染帧。原因如下:
- 将帧渲染时间降低到 16 毫秒以下可能不会产生视觉上的差异,但它会提高电池续航能力并减少发热问题。
- 它可能在您的设备上运行良好,但请考虑您要针对的最低端设备的性能。
- 随着 120fps 设备越来越普及,您将希望在 8 毫秒(总计)内渲染帧,以提供最流畅的体验。
如果您想知道为什么 60fps 能带来流畅的视觉体验,请观看视频为什么是 60fps?
陷阱
#如果您需要调整应用的性能,或者 UI 不如您预期的流畅,DevTools Performance 视图可以提供帮助!
此外,您 IDE 的 Flutter 插件也可能很有用。在 Flutter Performance 窗口中,勾选Show widget rebuild information复选框。此功能可帮助您检测何时渲染和显示帧的时间超过 16 毫秒。在可能的情况下,插件会提供指向相关提示的链接。
以下行为可能会对您的应用性能产生负面影响。
避免使用
Opacity
widget,尤其是在动画中避免使用。请改用AnimatedOpacity
或FadeInImage
。有关更多信息,请参阅 Opacity 动画的性能注意事项。在使用
AnimatedBuilder
时,避免在 builder 函数中放置一个会构建与动画无关的 widget 的子树。这个子树会为动画的每一次 tick 而重建。相反,一次性构建该子树的一部分,并将其作为子项传递给AnimatedBuilder
。有关更多信息,请参阅 性能优化。在动画中避免 clipping。如果可能,请在动画化图像之前对其进行预裁剪。
如果大多数子项不在屏幕上可见,请避免使用具有具体
List
子项的构造函数(例如Column()
或ListView()
),以避免构建成本。避免重写
Widget
对象上的operator ==
。虽然它看起来可以避免不必要的重建,但实际上它会损害性能,因为它会导致 O(N²) 的行为。此规则的唯一例外是叶子 widget(没有子项的 widget),在特定情况下,比较 widget 的属性可能比重建 widget 更有效,并且 widget 的配置很少更改。即使在这种情况下,通常也最好依赖于缓存 widget,因为即使一次对operator ==
的重写也可能导致全局性能下降,因为编译器无法再假定调用始终是静态的。
资源
#有关更多性能信息,请参阅以下资源:
- AnimatedBuilder API 页面的性能优化。
- Opacity API 页面的 Opacity 动画性能注意事项。
- ListView API 页面的子元素生命周期和如何高效加载它们。
StatefulWidget
的性能注意事项。- 优化 Flutter Web 加载速度的最佳实践