关于javascript:Flutter-性能优化打造高性能-widget

38次阅读

共计 4928 个字符,预计需要花费 13 分钟才能阅读完成。

作者:凹凸曼 – Bruce

本文是 Flutter 性能优化系列文章之一,记录了 Flutter 团队优化 Flutter Gallery(https://gallery.flutter.dev/#/)的实际。本文次要介绍了如何打造高性能的 widget。原文链接:https://medium.com/flutter/bu…

所有无状态和有状态 widget 都会实现 build() 办法,这个办法决定了它们是如何渲染的。app 中的一屏就可能有成千盈百个部件,这些部件可能只会构建一次,或者在有动画或者某种特定的交互状况下,也有可能构建屡次。如果想构建疾速的 widget,你肯定要很审慎地抉择构建哪些 widget,以及在什么时候构建。

这篇文章次要探讨只构建必要的和只在必要时构建,而后会分享咱们是如何应用这个方法来显著进步 Flutter Gallery 的性能。咱们还会分享一些高级技巧用于诊断你的 web app 中相似的问题。

只在必要时构建

一个重要的优化办法是,只在相对必要时才构建 widget。

审慎地调用 setState()

调用 setState 办法会引起 build() 办法调用。如果调用太屡次,会使性能变慢。

看一下上面的动画,显示在后面的彩色 widget 向下滑动,露出前面相似棋盘的面板,相似于 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(),)
   ],
 ),

你可能会像以下这样写父 widget,但在这个场景下,这样是谬误的:

// BAD CODE
@override
void initState() {super.initState();
  _animationController = AnimationController(duration: Duration(seconds: 3),
    vsync: this,
  );
  _animationController.addListener(() {setState(() {// Rebuild when animation ticks});
  });
}

这样性能并不好。为什么?因为动画在做不必要的工作。

以下是有问题的代码:

// BAD CODE
_animationController.addListener(() {setState(() {// Rebuild when animation ticks.});
});
  • 这种类型的动画只在你须要让整个 widget 动起来时才举荐应用,但这并不是咱们在这种布局中须要的。
  • 在动画监听器中调用 setState() 会引起整个 Stack 从新构建,这是齐全没必要的
  • PositionedTransition 部件曾经一个 AnimatedWidget 了,所以它会在动画开始的时候主动从新构建
  • 不须要在这里调用 setState()

即便前面的组件是很繁忙的,后面的组件动画也能够达到 60 FPS。更多无关正当地调用 setState 办法的内容,请看 Flutter 卡顿的动画:你不该这样 setState

只构建必要的局部

除了只在必要的时候进行构建,你还须要只构建 UI 中变动的局部。接下来的章节次要关注如何创立一个高性能的 list。

优先应用 ListView.builder()

首先,让咱们简略地看看显示 list 的根底:

  • 竖 list 应用 Column
  • 如果 list 须要滚动,应用 ListView
  • 如果 list 有很多 item,应用 ListView.builder,这个办法会在 item 滚动进入屏幕的时候才创立 item,而不是一次性创立所有的 item。这在 list 很简单和 widget 嵌套很深的状况下,有显著的性能劣势。

为了解释多 item 状况下 ListView.builder 相较于 ListView 的劣势,咱们来看几个例子。

在这个 DartPad 例子中运行以下 ListView。你能够看到 8 个 item 都创立好了。(点击左下角的 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。你能够看只有可见的 item 被创立了,当你滚动时,新的 item 才被创立。

ListView.builder(itemBuilder: (context, index) {return _ListItem(index: index);
  },
  itemCount: 8,
);

当初,运行这个例子。在这里例子中,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) {
  // 这种状况下 ListView.builder 并不会有性能上的益处
  return ListView.builder(itemBuilder: (context, index) {return listItems[index];
    },
    itemCount: 8,
  );
}

更多无关提早构建 list 的内容,请看 Slivers, Demystified。

怎么通过一行代码,晋升超过两倍的性能

Flutter Gallery 反对超过 100 个地区;这些地区,可能你也猜到了,是通过 ListView.builder() 来展现的。通过查看 widget 从新构建的次数,咱们留神到这些 item 会在启动时进行不必要的构建。这个状况有点难发现,因为这些 item 藏在折叠了两层的菜单下:设置面板和地区列表。(起初咱们发现,因为应用了 ScaleTransitioin,设置面板在不可见状态下也会进行渲染,意味着它会一直地被构建)。

通过简略地将 ListView.builderitemCount 在未开展状态下设置为 0,咱们确保了 item 只会在开展的、可见的设置面板中才进行构建。这一行改变进步了在 web 环境下渲染工夫将近两倍,其中的要害是定位适度的 widget 构建。

如何查看 widget 的构建次数

尽管 Flutter 的构建是很高效的,然而也会呈现适度构建导致性能问题的状况。有几种办法能够帮忙定位适度的 widget 构建:

应用 Android Studio/IntelliJ

Android Studio 和 IntelliJ 开发者能够应用自带的工具来查看 widget 从新构建信息。

批改 Flutter 框架自身

如果应用的不是以上的编辑器,或者心愿能够晓得 web 环境下 widget 的从新构建次数,你能够在 Flutter 框架中退出几行简略的代码。

先看一下输入成果:

RaisedButton 1
RawMaterialButton 2
ExpensiveWidget 538
Header 5

先定位到文件:<Flutter path>/packages/flutter/lib/src/widgets/framework.dart,而后退出以下代码。这些代码会在启动时统计 widget 的构建次数,并在一段时间(这里设置的是 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');
      }
    });
  });
}

而后,批改 StatelessElementStatelessElementbuild 办法来调用 _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 文件。

须要留神的是,几次从新构建不肯定会引起问题,然而这个方法能够通过验证不可见的 widget 是否在构建来帮你 debug 性能问题。

web 专用 tips:你能够增加一个 resetOutput 函数(能够在浏览器的控制台中调用)来获取随时获取 widget 的构建次数。

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 文件。

结语

高效的性能调优须要咱们明确底层的工作原理。文章里的 tips 能够帮忙你决定什么时候构建 widget 来使你的 app 在所有场景都放弃高性能。

这篇文章是咱们在进步 Flutter Gallery 性能中学习到的系列内容之一。心愿对你有所帮忙,能让你学到能够在你的 Flutter app 中用上的内容。系列文章如下:

  • Flutter 性能优化系列之 tree shaking 和提早加载
  • Flutter 性能优化系列之图片占位符、预缓存和禁用导航过渡动画
  • Flutter 性能优化系列之打造高性能 widget(本文)

你还能够查看实用所有程度开发者的 Flutter UI 性能文档。

欢送关注凹凸实验室博客:aotu.io

或者关注凹凸实验室公众号(AOTULabs),不定时推送文章。

正文完
 0