flutter 性能实践思路

松新
2023-12-01

原文

前言

Generally, Flutter applications are performant by default, so you only need to avoid common pitfalls to get excellent performance. These best recommendations will help you write the most performant Flutter app possible.
通常,Flutter app默认是高性能的,因此你只需要规避常见的陷阱,即可获得出色的性能。 这些最佳建议将帮助你编写出性能绝佳的 Flutter app。

If you are writing web apps in Flutter, you might be interested in a series of articles, written by the Flutter Material team, after they modified the Flutter Gallery app to make it more performant on the web:
如果你正在使用 Flutter 编写 Web app,你可能会对 Flutter Material 团队在修改 Flutter Gallery app以使其在 Web 上的性能更高之后,再撰写的一系列文章感兴趣:(因为笔者是android flutter 开发,所以这三篇web的文章就没有顺带翻译)

Optimizing performance in Flutter web apps with tree shaking and deferred loading(需翻墙)

Improving perceived performance with image placeholders, precaching, and disabled navigation transitions(需翻墙)

Building performant Flutter widgets(需翻墙)

Best practices - 最佳实践

How do you design a Flutter app to most efficiently render your scenes? In particular, how do you ensure that the painting code generated by the framework is as efficient as possible? Here are a few things to consider when designing your app:
你会怎么设计一个 Flutter app来最有效地render你的scene? 特别是,如何确保framework生成的paint代码尽可能高效? 在设计app时,需要考虑以下几点:

Controlling build() cost- 控制build()耗时

  1. Avoid repetitive and costly work in build() methods since build() can be invoked frequently when ancestor Widgets rebuild.
    避免在 build() 方法中重复和耗时的工作,因为 build() 可以在祖先 Widget 重建时会被频繁的调用。

  2. Avoid overly large single Widgets with a large build() function. Split them into different Widgets based on encapsulation but also on how they change:
    避免使用大型的build() 函数,那种带着庞大的单个Widget。 可以根据封装原则,以及它们的变化方式,把它们拆分为不同的 Widget:
    When setState() is called on a State, all descendent widgets rebuild. Therefore, localize the setState() call to the part of the subtree whose UI actually needs to change. Avoid calling setState() high up in the tree if the change is contained to a small part of the tree.
    当在State上调用 setState() 时,所有后代Widget都会rebuild。 因此,将 setState() 的调用局部化到 UI 实际需要更改的子树部分。 如果需要刷新的内容只是树的一小部分,就避免在树的高层级处调用 setState()。

The traversal to rebuild all descendents stops when the same instance of the child widget as the previous frame is re-encountered. This technique is heavily used inside the framework for optimizing animations where the animation doesn’t affect the child subtree. See the TransitionBuilder pattern and the source code for SlideTransition, which uses this principle to avoid rebuilding its descendents when animating.
当再遇到与前一帧完全相同的子Widget实例时,就会停止遍历rebuild所有的后代。 这种技术在framework大量使用,并用于优化animation,不影响animation的子树。 请参阅 TransitionBuilder 模式和 SlideTransition 的源代码,它使用此原则来避免在animation中rebuild其后代。

Also see:
更多可见:

Performance considerations, part of the StatefulWidget API doc
StatefulWidget的性能注意事项,StatefulWidget API文档的一部分

Apply effects only when needed - 仅在需要时使用特效

Use effects carefully, as they can be expensive. Some of them invoke saveLayer() behind the scenes, which can be an expensive operation.
谨慎使用特效,因为它们可能开销大。 其中一些在scene的背后调用 saveLayer(),这可能是一项开销大的操作。

Why is savelayer expensive? - 为什么savelayer开销大

Calling saveLayer() allocates an offscreen buffer. Drawing content into the offscreen buffer might trigger render target switches that are particularly slow in older GPUs.
调用 saveLayer() 分配一个offscreen buffer。 将内容绘制到offscreen buffer可能会触发在旧GPU 中特别慢的渲染目标切换。

Some general rules when applying specific effects:
应用特定的特效时的一些一般规则:

  1. Use the Opacity widget only when necessary. See the Transparent image section in the Opacity API page for an example of applying opacity directly to an image, which is faster than using the Opacity widget.
    仅在必要时使用Opacity Widget。 有关将Opacity直接应用于Image的示例,请参阅 Opacity API 页面中的Opacity Image 部分,这比使用Opacity Widget更快
  2. Clipping doesn’t call saveLayer() (unless explicitly requested with Clip.antiAliasWithSaveLayer) so these operations aren’t as expensive as Opacity, but clipping is still costly, so use with caution. By default, clipping is disabled (Clip.none), so you must explicitly enable it when needed.
    Clipping 不会调用 saveLayer()(除非使用 Clip.antiAliasWithSaveLayer 明确要求),因此这操作不像 Opacity 那样开销大,但Clipping仍然有一定开销,因此请谨慎使用。 默认情况下,clipping 是被禁用的 (Clip.none),因此你需要时必须显式启用它。

Other widgets that might trigger saveLayer() and are potentially costly:
其他可能触发 saveLayer() 并且可能成本高昂的Widget:

  1. ShaderMask
  2. ColorFilter
  3. Chip—might cause call to saveLayer() if disabledColorAlpha != 0xff
    Chip—如果disabledColorAlpha != 0xff,可能会调用saveLayer()。
  4. Text—might cause call to saveLayer() if there’s an overflowShader
    Text—如果存在overflowShader,可能会调用 saveLayer()

Ways to avoid calls to saveLayer():
避免调用 saveLayer() 的方法:

  1. To implement fading in an image, consider using the FadeInImage widget, which applies a gradual opacity using the GPU’s fragment shader. For more information, see the Opacity docs.
    要在image中实现淡入淡出效果,请考虑使用 FadeInImage Widget,该Widget使用 GPU 的fragment shader,它使用于渐变的opacity。 有关详细信息,请参阅Opacity文档。

  2. To create a rectangle with rounded corners, instead of applying a clipping rectangle, consider using the borderRadius property offered by many of the widget classes.
    要创建带圆角的矩形,而非使用clipp方式改变矩形,请考虑使用很多Widget类都会提供的borderRadius 属性。

Render grids and lists lazily - 延迟渲染 grid和lists

Use the lazy methods, with callbacks, when building large grids or lists. That way only the visible portion of the screen is built at startup time.
在构建大型grid或list时,使用带有回调的懒加载方法。 这样,在启动时就只会build屏幕中的可见部分。

Also see:
另见:

Creating a ListView that loads one page at a time a community article by AbdulRahman AlHamali
创建一次加载一个页面的 ListView AbdulRahman AlHamali 的社区文章

Build and display frames in 16ms-16毫秒内完成一帧刷新

Since there are two separate threads for building and rendering, you have 16ms for building, and 16ms for rendering on a 60Hz display. If latency is a concern, build and display a frame in 16ms or less. Note that means built in 8ms or less, and rendered in 8ms or less, for a total of 16ms or less. If missing frames (jankyness) is a concern, then 16ms for each of the build and render stages is OK.
由于 build和render有两个单独的线程,因此你有 16 毫秒的build时间和 16 毫秒的时间render在60Hz 显示器上。 如果延迟了,这是一个问题,请在 16 毫秒或更短的时间内构建和显示一帧。 请注意,这意味着在 8 毫秒或更短的时间内build,并在 8 毫秒或更短的时间内render,总共 16 毫秒或更短的时间。 如果缺少帧(jankyness),这是一个问题,每个build和render阶段的 16 毫秒是可接受的。

If your frames are rendering in well under 16ms total in profile mode, you likely don’t have to worry about performance even if some performance pitfalls apply, but you should still aim to build and render a frame as fast as possible. Why?
如果你的一帧在profile模式下的总渲染时间远低于 16 毫秒,那么即使存在一些性能缺陷,你也可能不必担心性能,但你仍然应该以尽可能快的速度build和render帧为目标。 为什么?

  1. Lowering the frame render time below 16ms might not make a visual difference, but it improves battery life and thermal issues.
    将帧渲染时间降低到 16 毫秒以下可能不会产生视觉差异,但它可以改善电池寿命和散热问题。
  2. It might run fine on your device, but consider performance for the lowest device you are targeting.
    它可能在你的设备上运行良好,但请考虑你所针对的最低端设备的性能。
  3. When 120fps devices become widely available, you’ll want to render frames in under 8ms (total) in order to provide the smoothest experience.
    当 120fps 设备广泛使用时,你需要在 8 毫秒(总计)内render一帧,以提供最流畅的体验。

If you are wondering why 60fps leads to a smooth visual experience, see the video Why 60fps?
如果你想知道为什么 60fps 会带来流畅的视觉体验,请参阅视频为什么 60fps?(需翻墙)

Pitfalls - 陷阱

If you need to tune your app’s performance, or perhaps the UI isn’t as smooth as you expect, the Flutter plugin for your IDE can help. In the Flutter Performance window, enable the Show widget rebuild information check box. This feature helps you detect when frames are being rendered and displayed in more than 16ms. Where possible, the plugin provides a link to a relevant tip.
如果你需要调整app的性能,或者 UI 可能没有你期望的那么流畅,你的 IDE 的 Flutter 插件可以提供帮助。 在 Flutter Performance 窗口中,启用 Show widget rebuild information 复选框。 此功能可帮助你检测到渲染和显示时间超过 16 毫秒的帧数。 在可能的情况下,该插件会提供指向相关提示的链接。

The following behaviors might negatively impact your app’s performance.
以下行为可能会对你的app的性能产生负面影响。

  1. Avoid using the Opacity widget, and particularly avoid it in an animation. Use AnimatedOpacity or FadeInImage instead. For more information, see Performance considerations for opacity animation.
    避免使用 Opacity Widget,尤其是在动画中避免使用它。 请改用 AnimatedOpacity 或 FadeInImage。 有关详细信息,请参阅opacity animation性能注意事项

  2. When using an AnimatedBuilder, avoid putting a subtree in the builder function that builds widgets that don’t depend on the animation. This subtree is rebuilt for every tick of the animation. Instead, build that part of the subtree once and pass it as a child to the AnimatedBuilder. For more information, see Performance optimizations.
    使用 AnimatedBuilder 时,避免在builder函数中放置不依赖animation的Widget子树。 这个子树会在animation的每个tick 都rebuilt。 相反,只build子树一次,并且将其作为child传递给 AnimatedBuilder。 有关详细信息,请参阅AnimatedBuilder-简单的性能优化

  3. Avoid clipping in an animation. If possible, pre-clip the image before animating it.
    避免在动画中clipping。 如果条件允许,请在animate启动之前pre-clip image。

  4. Avoid using constructors with a concrete List of children (such as Column() or ListView()) if most of the children are not visible on screen to avoid the build cost.
    如果大多数子项在屏幕上不可见,则避免使用具有具体子项列表的构造函数(例如 Column() 或 ListView())以避免build 的开销。

Resources - 资料

For more performance info, see the following resources:
有关更多性能信息,请参阅以下资料:

  1. Performance optimizations in the AnimatedBuilder API page
    AnimatedBuilder-简单的性能优化
  2. Performance considerations for opacity animation in the Opacity API page
    Opacity API 页面中 opacity animation的性能注意事项
  3. Child elements’ lifecycle and how to load them efficiently, in the ListView API page
    ListView API 页面中的ListView item的element生命周期以及如何有效地加载它们
  4. Performance considerations of a StatefulWidget
    StatefulWidget 的性能注意事项
 类似资料: