原文作者:medium.com/@guidez
发布时间:2020年7月23日 - 7分钟阅读
本文是 Flutter Material 团队致力于使 Flutter Gallery 应用在网络上的性能更强之后开发的系列文章的一部分。然而,本文中的提示适用于所有Flutter应用程序。跳到结尾找到本系列的其他文章。
作者:Anthony Robledo & Pierre-Louis Guidez。
所有无状态和有状态的widget都实现了build()
方法,定义了它们的渲染方式。一个应用程序上的一个屏幕可以有数百甚至数千个widget,这些widget可能只被构建一次,或者如果有动画或某种交互,则被构建多次。这些widgets可能只被构建一次,如果有动画或某种交互,则会被多次构建。虽然在Flutter中构建widget的速度相对较快,但你必须对何时以及选择构建什么保持警惕。
这篇文章讲的是只构建你需要的东西,并且只在你需要的时候构建。然后,我们将分享我们如何使用这种方法来实现Flutter Gallery Web应用的性能显著提升。我们还将分享专业的技巧,告诉你如何诊断你的web应用中的类似问题。
只有在必要的时候才进行构建
一个重要的优化是只有在绝对必要的时候才构建widget。
谨慎地调用setState()
调用setState
会安排调用build()
方法。太频繁地这样做会减慢屏幕的性能。
考虑下面的动画,其中正面(黑屏)的显示被动画化为向下滑动以显示背面(棋盘),类似于bottom sheet的行为。前面的widget很简单,但后面的widget很忙。
流畅的动画
Stack(
children: [
Back(),
PositionedTransition(
rect: RelativeRectTween(
begin: RelativeRect.fromLTRB(0, 0, 0, 0),
end: RelativeRect.fromLTRB(0, MediaQuery.of(context).size.height, 0, 0),
).animate(_animationController),
child: Front(),
)
],
),
你可能会想把父部件设置成如下的样子,但在这种情况下,这是错误的!
// 错误的代码
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: Duration(seconds: 3),
vsync: this,
);
_animationController.addListener(() {
setState(() {
// 当动画点击时重建。
});
});
}
这样性能差!为什么?
因为动画在做不必要的工作。
抖动的动画
以下是有问题的代码。
// 错误的代码
_animationController.addListener(() {
setState(() {
// 当动画点击时重建。
});
});
- 当你需要对整个widget进行动画时,建议使用这种动画风格,但我们在这里做的不是这个。
- 在动画监听器中调用
setState()
会导致整个Stack
被重建,而这是没有必要的。 PositionedTransition
widget已经是一个AnimatedWidget
,所以当动画点击时,它会自动重建。- 调用
setState()
在这里其实是不需要的!
即使后面的小组件很忙,它也能以60 FPS的速度流畅地制作动画。关于明智地调用setState的更多信息,请参见Flutter Laggy Animations: How Not To setState。
只构建必要的东西
除了只在需要的时候构建,你还想只构建UI中实际变化的部分。下面一节主要介绍如何创建执行力强的列表。
优先使用ListView.builder()
首先,让我们简单介绍一下显示列表的基础知识。
- 要垂直布局列表项,使用
Column
。 - 如果列表应该是可滚动的,则使用
ListView
来代替。 - 如果列表中包含许多项目,使用
ListView.builder
构造函数,它可以在项目滚动到屏幕上时创建项目,而不是一次全部创建。这对于复杂的列表项和深层的小组件子树有明显的性能优势。 为了说明当你有大量列表项时,ListView.builder
比ListView
的好处,我们来看几个例子。 在DartPad中运行以下ListView
示例。观察所有8个项目都被创建了。(点击左下角的Console显示控制台,然后点击Run。输出窗口没有滚动条,但你可以滚动内容并观察控制台,看看什么是创建的,什么时候建立的)。)
ListView(
children: [
_ListItem(index: 0),
_ListItem(index: 1),
_ListItem(index: 2),
_ListItem(index: 3),
_ListItem(index: 4),
_ListItem(index: 5),
_ListItem(index: 6),
_ListItem(index: 7),
],
);
接下来,在DartPad中运行ListView.builder
示例。请注意,只有可见的项目被创建。当你滚动时,它会创建(并构建)新的行。
ListView.builder(
itemBuilder: (context, index) {
return _ListItem(index: index);
},
itemCount: 8,
);
现在,在DartPad中运行这个例子,其中ListView
的子代是提前创建的,在创建ListView本身的时候一次性创建完毕。在这种情况下,使用ListView
构造函数更有效率。
final listItems = [
_ListItem(index: 0),
_ListItem(index: 1),
_ListItem(index: 2),
_ListItem(index: 3),
_ListItem(index: 4),
_ListItem(index: 5),
_ListItem(index: 6),
_ListItem(index: 7),
];
@override
Widget build(BuildContext context) {
// This offers no benefit, it is actually more efficient to use the ListView constructor instead.
return ListView.builder(
itemBuilder: (context, index) {
return listItems[index];
},
itemCount: 8,
);
}
关于懒惰地构建列表的更多信息,请参见Slivers, Demystified。
我们如何用一行代码将Flutter Gallery的网页渲染时间提高了2倍以上。
Flutter Gallery支持超过100个locales;这些locales使用--你猜对了--ListView.builder
来列出。通过获取widget重建信息,我们注意到这些列表项在启动时被不必要地构建。这些项目的罪魁祸首并不明显,因为它们在两个级别的折叠菜单中:设置面板本身,以及locale扩展磁贴(事实证明,设置面板使用ScaleTransition
被渲染为 "不可见",这意味着它非常被构建)。
Flutter图库设置面板,扩展了地域选项。
通过简单地将ListView.builder
的itemCount
设置为0,对于非展开的设置类别,我们确保列表项只为展开的、可见的类别构建。解决了这个问题的单行PR将网络上的渲染时间提高了2倍以上。关键在于识别过度的小部件构建。
查看您的应用程序的小部件构建计数
虽然Flutter的构建效率很高,但有些情况下,过度构建会导致性能问题。有几种方法可以识别过度的widget重构。
通过使用Android Studio/IntelliJ
Android Studio和IntelliJ开发者可以使用内置的工具来显示Widget重建信息。
通过修改框架
如果你使用不同的编辑器,或者想知道web的widget重建信息,你可以通过在框架中添加一些代码来实现。
输出示例:
RaisedButton 1
RawMaterialButton 2
ExpensiveWidget 538
Header 5
找到<Flutter path>/packages/flutter/lib/src/widgets/framework.dart
。添加下面的代码,它在启动时统计widgets的构建次数,并在一定的持续时间(这里是10秒)后输出结果。
bool _outputScheduled = false;
Map<String, int> _outputMap = <String, int>{};
void _output(Widget widget) {
final String typeName = widget.runtimeType.toString();
if (_outputMap.containsKey(typeName)) {
_outputMap[typeName] = _outputMap[typeName] + 1;
} else {
_outputMap[typeName] = 1;
}
if (_outputScheduled) {
return;
}
_outputScheduled = true;
Timer(const Duration(seconds: 10), () {
_outputMap.forEach((String key, int value) {
switch (widget.runtimeType.toString()) {
// Filter out widgets whose build counts we don't care about
case 'InkWell':
case 'RawGestureDetector':
case 'FocusScope':
break;
default:
print('$key $value');
}
});
});
}
然后,修改StatelessElement
和StatefulElement
的build
方法,调用_output(widget)
。
class StatelessElement extends ComponentElement {
...
@override
Widget build() {
final Widget w = widget.build(this);
_output(w);
return w;
}
class StatefulElement extends ComponentElement {
...
@override
Widget build() {
final Widget w = _state.build(this);
_output(w);
return w;
}
请看生成的framework.dart文件。
需要注意的是,多次重建并不一定说明有问题,但它可以通过验证不可见的小组件没有被构建来帮助调试性能问题。然而,它可以帮助调试性能问题,例如,通过验证不可见的部件没有被构建。
仅适用于web的提示:你可以添加一个resetOutput
函数(可以从浏览器的开发工具中调用)来获取任何时间点的widget build counts。
import 'dart:js' as js;
void resetOutput() {
_outputScheduled = false;
_outputMap = <String, int>{};
}
void _output(Widget widget) {
// Add this line
js.context['resetOutput'] = resetOutput;
...
请看产生的framework.dart文件。
结束语
有效的性能调试需要了解引擎盖下发生了什么。这些提示可以帮助决定如何实现你的下一个构建方法,以便你的应用程序在所有场景下保持性能。
这篇文章是关于我们在提高Flutter Gallery的性能时学到的系列文章的一部分。创建高性能的Flutter web应用系列中的文章:
- 使用剪枝和延迟加载优化Flutter Web应用程序的性能。
- 使用图像占位符、预缓存和禁用的导航过渡来改善感知性能。
- 构建性能优异的Flutter widget(本文)
如果想了解更多的一般信息,关于UI性能的Flutter文档是所有级别的开发人员开始的好地方。
感谢Shams Zakhour编辑本文,感谢Ferhat Buyukkokten提供的调试技巧。
通过www.DeepL.com/Translator(免费版)翻译