1. Introduction

The project flutter_DEER has been open source for nearly a year, and has received 3,100 + stars so far, which is undoubtedly the biggest recognition for this project. It’s pretty much the same from a year ago in terms of functionality and UI. In the meantime, I’ve been refining it, hoping for better performance and better experience. This article focuses on deer in UI fluency optimization details, practice – based, supplemented by source code. Share, hope to inspire and help you.

Since we need to optimize, we must first master the methods of locating problems and analyzing performance problems, so that we can compare the effects before and after optimization. I won’t go into the details here. You can refer to the official documentation or watch this video: Flutter Performance Tests and Theories.

In official documentation, performance analysis is required to ensure that you are using a real machine and running in profile mode. However, we can use debug mode to find lag, because I think it can amplify your “problem”.

Now let’s get down to business. To be colloquial, I will refer to the build of the Flutter as “refresh”. This source is based on Flutter SDK version 1.17.0.

2. Control the refresh range

We can easily refresh the page using the setState method, but try to control the refresh scope. Let me give you an example:

When registering an account, you usually need to obtain a verification code. There will be a countdown function, so we need to refresh the countdown number every second and display it.

If the countdown logic is handled as you place it on the registration page, then every time you setState it will be a whole page refresh. This whole page refresh is obviously unnecessary. And it doesn’t give you a sense of Catton, so it’s hard to see,

The solution is to wrap the countdown button separately into a StatefulWidget that uses the setState refresh to control the refresh scope.

Similarly, you can use state management frameworks such as Providers to implement partial flushes. Control your refresh range accurately and never setState to refresh a shuttle.

3. Control the refresh times

Controlling the number of flusher (to avoid invalid flusher) is even more important than controlling the scope of the flusher. I have sorted out four points in this part, and I will explain them one by one.

Demand for control

Or the above registration scene, here we need to input content to meet the conditions can click the registration button.

So what we do is listen inTextFieldThe text input, each time the input to determine whether to meet the conditions, update the button can be clicked state. The code looks like this:

bool _clickable = false;

void _verify(a) {
  String phone = _phoneController.text;
  String vCode = _vCodeController.text;
  String password = _passwordController.text;
  _clickable = true;
  if (phone.isEmpty || phone.length < 11) {
    _clickable = false;
  }
  if (vCode.isEmpty || vCode.length < 6) {
    _clickable = false;
  }
  if (password.isEmpty || password.length < 6) {
    _clickable = false;
  }
  setState(() {
    
  }); 
}

MyButton(
  onPressed: _clickable ? _register : null,
  text: 'registered'.)Copy the code

I can actually optimize this a little bit. Because every input is now bound to refresh, we can refresh when the _clickable parameter changes to avoid an invalid refresh. The optimized code is as follows:

void _verify(a) {
  String phone = _phoneController.text;
  String vCode = _vCodeController.text;
  String password = _passwordController.text;
  bool clickable = true;
  if (phone.isEmpty || phone.length < 11) {
    clickable = false;
  }
  if (vCode.isEmpty || vCode.length < 6) {
    clickable = false;
  }
  if (password.isEmpty || password.length < 6) {
    clickable = false;
  }
  // refresh if the status is inconsistent
  if (clickable != _clickable) {
    setState(() {
      _clickable = clickable;
    });
  }
}
Copy the code

Imagine how many flushes you can reduce with this simple process.

Similarly, in CustomPainter there is a shouldRepaint override method that we can control whether or not The CustomPainter redraws as needed.

Pre-built widgets

The use of animation is common in real development, but it can cause unnecessary refreshes and even lag if used incorrectly.

As an example in Deer, the merchandise list page has an in-and-out animation for the merchandise operation menu (I won’t talk about the implementation here, you can read the source code if you are interested). It starts as follows:


AnimatedBuilder(
  animation: animation,
  builder:(_, __) {
    returnMenuReveal( revealPercent: animation.value, child: _buildGoodsMenu(context), ); })Copy the code

The effect is as follows:This animation looks pretty smooth. Performance chart at the top (Performance Overlay), UI takes an average of 7.2ms/frame. That’s pretty good by 16ms security standards.

But let’s look at the build count (one inbound and one outbound) :

A closer look at this is a bit of a problem. When the animation executes, we only expect the mutable part to refresh, but actually the buttons in the menu refresh the build as well.

The best way to optimize is to pre-build the button in the menu, execute the _buildGoodsMenu(Context) method before the AnimatedBuilder and pass it in or put it in the Child of the AnimatedBuilder.


AnimatedBuilder(
  animation: animation,
  child: _buildGoodsMenuContent(context), // <----- put it here
  builder:(_, child) {
    return MenuReveal(
      revealPercent: animation.value,
      child: child  // <---- used here); })Copy the code

The effect is as follows:

You can see that the UI thread takes around 6ms/frame. This is still a large increase (around 16%), although it is imperceptible to the user.

Take a look at the build count again:

Then the reason for the promotion is found, because unnecessary builds are avoided.So for such child widgets that do not rely on animation, pre-building it can significantly improve performance.

Keep an eye out for more builder/ Child patterns like this.

reuse

  • Try to useconstTo define invariant widgets, which is equivalent to caching a Widget and reusing it.

I read a blog where the author tested building 1000 duplicate ICONS on a page and found that using the const constructor resulted in an 8.4% higher FPS and a 20% lower memory usage.

Of course, as the author points out, it’s unrealistic to have 1000 widgets on a page. In fact, the reason for this point is also to hope that we can develop a good habit.

  • addGlobalKeyWidgets can also be reused. This usage scenario is relatively small, so you can look at it. Related content links:Talk about the Key in Flutter

RepaintBoundary

I have described this in detail before and you can look at it directly: Talk about RepaintBoundary in Flutter. I will not repeat it here. Proper use of RepaintBoundary can reduce unnecessary refreshes and improve performance.

4. Load the policy

According to the need to load

It is recommended to use listView. builder to implement lists dynamically rather than using ListView to create them statically. Note that when you use the itemBuilder of ListView.builder to build an Item, don’t pre-build the Widget. Similar widgets include PageView. Builder and GridView. Builder.

PS: Loading on demand is a strategy that doesn’t rely solely on these types of widgets. For example, in the previous post on AliAliflutter, there was an optimisation of loading images into lists. By judging the on-screen and off-screen images, images can be recycled reasonably, which reduces memory fluctuation and improves performance.

Peak load

The purpose of off-peak loading is to avoid the lag phenomenon caused by a large number of builds at the same time. Here’s an example:

Using the PageView. Builder Widget, I noticed a lag when I swiped left and right to switch pages. Two problems are found in the analysis of timeline. One is that switching pages is complicated and time-consuming. Second, the page is built at the time point in the slide.

Page complex problems I have carried out a certain optimization, although there is an effect, but there are still stuck. So we can only optimize for the second point, so let’s take a look at PageView. Related source code:

return NotificationListener<ScrollNotification>(
  onNotification: (ScrollNotification notification) {
    if (notification.depth == 0&& widget.onPageChanged ! =null && notification is ScrollUpdateNotification) {
      final PageMetrics metrics = notification.metrics;
      final int currentPage = metrics.page.round();
      if (currentPage != _lastReportedPage) {
        _lastReportedPage = currentPage;
        widget.onPageChanged(currentPage);
      }
    }
    return false;
  },
  child: Scrollable(),
);
Copy the code

The code is simple, if we set a listener for onPageChanged, the page number for the current page is computed in a slide (ScrollUpdateNotification) and returned (round method, rounded). So in the middle of the swipe, onPageChanged will call back the result, where I triggered the page refresh code, causing a lag.

In fact, on Android I know, the default behavior is to load the page data after the swipe. So go along with that and tweak the loading strategy.

Modify the code as follows:

NotificationListener<ScrollNotification>(
  onNotification: (ScrollNotification notification) {
    if (notification.depth == 0 && notification is ScrollEndNotification) {
      final PageMetrics metrics = notification.metrics;
      final int currentPage = metrics.page.round();
      if (currentPage != _lastReportedPage) {
        _lastReportedPage = currentPage;
        _onPageChange(currentPage);
      }
    }
    return false;
  },
  child: PageView.builder(),
)
Copy the code

We add a NotificationListener on PageView. Builder and change ScrollUpdateNotification to ScrollEndNotification. This customizes our slide listener events and keeps the UI smooth through staggered loading.

PS: One of the most important changes to the Flutter 1.17 was to delay image decoding while rolling at high speeds. This is also a staggered peak loading strategy.

5. Time consuming calculation

Instead of putting time-consuming calculations on the UI thread, we can put time-consuming calculations on the Isolate to perform (multi-threading).

Take an example from the Flutter source:

  Future<String> loadString(String key, { bool cache = true }) async {
    final ByteData data = await load(key);
    if (data == null)
      throw FlutterError('Unable to load asset: $key');
    if (data.lengthInBytes < 10 * 1024) {
      // 10KB takes about 3ms to parse on a Pixel 2 XL.
      // See: https://github.com/dart-lang/sdk/issues/31954
      return utf8.decode(data.buffer.asUint8List());
    }
    return compute(_utf8decode, data, debugLabel: 'UTF8 decode for "$key"');
  }

  static String _utf8decode(ByteData data) {
    return utf8.decode(data.buffer.asUint8List());
  }
Copy the code

Since utF8.decode takes about 3ms to process 10KB of data (mobile Pixel 2 XL), compute is used to put the time calculation into the Isolate for data exceeding 10KB. Different methods are selected according to the data size. The reason is that the establishment and use of Isolate also cost space and time. Therefore, although the Isolate is good, don’t abuse it!

Similarly, the JSON parsing in our project can be done this way to ensure that the UI doesn’t lag on some of the less efficient machines. The implementation can be seen: handle JSON data parsing in the background

Here I will briefly explain the reason: Dart code in Flutter application is executed in UI Runner, while Dart is single-threaded. Asynchronous Future tasks we usually use are executed in this single-threaded Event Queue through Event Loop in order. (This single-threaded model is the same as JS)

That is, even if we execute this calculation code asynchronously, the code takes too long, so there is no idle thread during this period of time. That is, the thread is overloaded. As a result, the calculation of the layout of the Widget cannot be executed. The longer the time, the more obvious the lag phenomenon will be.

Therefore, the Isolate was used to handle the time calculation, and multithreading was used to execute the code in parallel.

You may have questions here, but my network request is also Dart code and sometimes quite time-consuming, why not page lag? This is because the network request is in the IO thread and does not occupy the UI thread. The actual network requests are not made at the Dart layer, the Dart code is just a layer of encapsulation, and the actual requests are implemented by the underlying operating system.

6.GPU

The above points are mostly about UI thread optimization. In fact, when we looked at Performance Overlay, we saw that sometimes the UI was smooth, but the GPU was time-consuming. This is mainly caused by GPU Runner, which may include time-consuming function calls to Skia’s saveLayer and clipPath.

SaveLayer allocates a new drawing buffer on the GPU (off-screen rendering) and switches drawing targets, which can be very time consuming on a GPU, especially on older devices.

Using clipPath affects every subsequent drawing instruction. Especially when the Path is complex, it is necessary to intersect the complex Path and remove the parts outside the Path.

A search for Canvas. SaveLayer in the Flutter source reveals some things to note:

  • SaveLayer is called when the overflow property of a Text is TextoverFlow.fade and the Text is out of range.

  • Using Clip. AntiAliasWithSaveLayer as shear behavior, will be called saveLayer (it is said that early version of the Flutter mostly use this way). Clip.hardEdge and Clip.antiAlias are recommended. These attributes are commonly used in clipping widgets such as ClipRect, ClipOval, and ClipPath.

  • Change the isEnabled property of RawChip to invoke saveLayer when enableAnimation is triggered.

ClipPath, on the other hand, is less time-consuming than saveLayer. But be aware of the clipping behavior. Preference is given to using the borderRadius property of BoxDecoration. For example, Inkwell’s borderRadius property can be used to trim its shape, or if the borderRadius really doesn’t fit, you can use the customBorder property (using clipPath).

You may be glad that I didn’t use any of this. Actually…

In addition to the explicit call time-consuming method mentioned above, there are also partial implicit calls (Opacity,ShaderMask,ColorFilter,PhysicalModel,BackdropFilter, etc.).

For example, the following description is shown in the Opacity document comment:

This class draws its children into an intermediate buffer and then blends them back into the transparent scene. For opacity values other than 0 and 1, this class is relatively expensive because it requires the child components to be drawn into an intermediate buffer. For opacity 0, no child component is drawn at all. For opacity 1.0, the child component is drawn immediately, without using an intermediate buffer.

Therefore, attention should be paid when Opacity is used and its properties are not 0 or 1. If you really need to use it, see if you can use an alternative:

  • AnimatedOpacity can be used if opacity changes are required.

  • For transparent images, you can modify the color property instead of wrapping a layer over Opacity. Such as:

Image.network(
  'https://xxxx.jpeg',
  color: Color.fromRGBO(255.255.255.0.5),
  colorBlendMode: BlendMode.modulate
)
Copy the code

PS: Although it seems that many widgets have performance issues, it is a case by case scenario. It’s just a reminder to think twice before using it and try to find alternatives, but it’s not a complete no-no. For example, BackdropFilter is used for Gaussian blur. CupertinoAlertDialog and CupertinoActionSheet use it, so we’re not going to stop using it.

Despite the above experience, the tools for monitoring and finding problems still need to be mastered. The following is a brief explanation, which will give you an in-depth look at Flutter’s high-performance graphical rendering.

inMaterialApp addcheckerboardOffscreenLayers: true To check if it’s usedsaveLayer(including explicit or implicit calls), if used, a “checkerboard grid” overlays it. Unfortunately, all I’ve found so far isBackdropFilterThe use of can be checked directly by this. The following figure shows the use ofCupertinoActionSheetThe effect of:sincecheckerboardOffscreenLayersRestricted, then it can be usedtimelineTo viewFlutterSkiaThe call. Here toCupertinoActionSheetExample of the pop-up process.

First run in profile mode:

flutter run --profile --trace-skia
Copy the code

There will be a link to “observatory” after successful installation: timelineThe performance is as follows:In the figureSkThe beginning isSkiaFunction, you can see the callsaveLayerMethods. But it’s not intuitive, it’s complicated. So you can do it by capturingSKPictureTo analyze each drawing instruction.

Continue with the following command:

flutter screenshot --type=skia --observatory-uri=uri
Copy the code

This URI is the link to the observatory.I’m going to generate one hereskpFormat the file in your project root directory and then upload the file tohttps://debugger.skia.org/(FQ required) for analysis.This analysis tool includes powerful functions such as playing the drawing instructions for pausing one by one, viewing the Clip area, and counting the number of times the instructions are called.

You can see the call in the figuresaveLayerMethod and number of calls. Using this analysis tool, you can understand the drawing process of the page in detail, so that we can remove unnecessary drawing parts and improve performance.

7. Other

  • Pay attention toFlatButtonSuch as the use of complex widgets.

For example,The order list in Deer had three buttons on Item, so they were used in the beginningFlatButtonI did, and it turned out that the page was a bit staid when sliding. Just usetimelineCheck it out:One at the time of most discoveriesFlatButtonIt took 1.5ms, 1ms on average. But since a screen typically displays three items, it’s not surprising that this doesn’t add up. And the reason is thatFlatButtonThis Widget has too many features and a complex hierarchy, resulting in Widget build time.

Then useGestureDetector + Container + TextImplement one of these buttons yourself to replace. Take a look at the effect again:After modification, build time is greatly reduced (0.3ms on average). You can see the hierarchy is a lot simpler. So usingFlatButtonNo problem, butPay attention to its complexity and use it wisely.

  • Use the StatelessWidget in preference to the StatefulWidget.

  • Specify the size of your widgets to avoid unnecessary Layout calculations. For example, itemExtent is used by ListView.

  • Try to avoid changing the depth of the subtree or changing the types of widgets in the subtree. Because this operation reconstructs, lays out, and draws the entire subtree.

    If you need to change the depth, consider adding GlobalKey to the common part of the subtree.

    Use Visibility if you need to change the type of Widget, such as to display hidden requirements. Think about the difference between these three ways:

      Column(
        children: <Widget>[
          if (_visible) const Text('1'),
          _visible ? const Text('2') : const SizedBox.shrink(a).Visibility(
            visible: _visible,
            child: const Text('3'),),,)Copy the code
  • Some Curves can be animated (fast then slow). In the same amount of time, the visual will be faster than linear animation, let a person feel smooth.


A few days ago, the Stable version of Flutter 1.17.0 was released, which also saw a number of performance improvements, including a Color implementation of Container. We believe that the Flutter experience will be much better in the future.

SOB :: SOB :: SOB :: SOB :: SOB :: SOB If you also have good optimization practices, welcome to discuss!

Finally, you can like the favorites to support a wave! Also support my Open source Flutter project, Flutter_deer. Well, see you next month


2020.08.06 update:

SDK 1.20.0 provides SkSL warm-up. If the Flutter application has a lag animation when it first runs, it can use the Skia Shading Language precompilation to make the Flutter faster by more than 2 times. See the official documentation for details. I tested in Flutter_deer.

Reference 8.

  • StatefulWidget performance considerations

  • Jank Flutter | performance optimization, how to avoid the application

  • Flutter Europe: Optimizing your Flutter App

  • Flutter Thread Management

  • Optimization of Flutter