About the Author | This article is jointly written by the Ctrip Train Ticket Flutter team. 1. BackgroundCtrip Train Tickets has implemented Flutter on a large scale in the list pages and main processes of more than ten core businesses. After more than a year of development and maintenance, we have summarized a set of effective performance optimization solutions. This article mainly introduces how to use performance analysis tools to identify, distinguish, and locate some performance problems, and find specific methods and code locations to help solve problems faster. In addition, we will also share some performance optimization cases and experience optimizations we have made, hoping to bring you some inspiration. 2. Rendering OptimizationFlutter rendering performance issues can be mainly divided into two types: GPU thread issues and UI thread (CPU) issues. The Performance Overlay tool can clearly distinguish them. If the UI thread chart is red or both charts are red, it means that the Dart code consumes a lot of resources and the code execution time needs to be optimized. Combined with the flame chart, analyzing the CPU call stack can easily find which method takes a long time, what the method name is, how deep the rendering level is, and can also compare before and after performance optimization. If only the GPU thread chart is red, it means that the rendered graphics are too complex and cannot be rendered quickly. Sometimes the construction of the Widget tree is simple, but the rendering of the GPU thread is very time-consuming. It is necessary to consider whether it is over-rendering, lack of component cache, and the rendering of multiple views such as Widget clipping and masking. 2.1 Selector controls the refresh rangeIn StatefulWidget, it is easy to render and refresh the interface through setState. Try to control the refresh range as much as possible to avoid unnecessary re-rendering of interface components, which will cause excessive GPU consumption and interface freeze. An example is shown below: When the interface scrolls, we need to listen to CustomerScrollView and then set the transparency of the top floating component to achieve the effect. The code is as follows: /// Animation distance The transparency is set according to the scrolling distance; however, setState will refresh the entire interface, and all components of the entire interface will be re-rendered. By checking the number of component renderings through Flutter Performance, we found that the entire interface is being refreshed. When we swiped the page multiple times, we found that many components were rendered multiple times, as shown in the following figure: Through DevTools, when sliding to change the transparency of the top, it is found that the FPS value is very low, and almost every frame exceeds 16ms. The flame graph is very deep, indicating that the rendering level is very deep, and the components of the entire interface are re-rendered from top to bottom, as shown in the figure: Now you can understand why the interface freezes when the user slides. This is mainly due to excessive rendering consumption and the failure to control the refresh range of the interface. When changing the top floating component, you only need to change the state of the top component, and there is no need to refresh the entire tree. The transformation strategy is to control the refresh range through the Provider's Selector, store the transparency value in a subclass of ChangeNotifier, and notify the interface to refresh through the notifyListeners() function when the transparency changes. The monitoring code is as follows: void addScrollListenerForTopTitle ( BuildContext context ) { Transparency gradient component: Selector < TopTabStatusViewModel , int > ( builder : ( context , alpha , child ) { After the transformation, you can see that when the interface slides, only the components that need to change transparency are re-rendered, and the component reconstruction status is shown in the following figure: The flame graph looks like this: This greatly reduces the scope of component reconstruction, and only loads on demand each time, significantly reducing the number of build levels and the total time consumption. Therefore, when rendering the interface, the starting point of the Widget Tree traversal should be reduced as much as possible to reasonably control the scope of reconstruction. 2.2 setState reduces refresh granularityAs shown in the figure, there is a dynamic carousel effect, which needs to be rotated every 2 seconds. The implementation method is to use a Timer to setState the text every 2 seconds to achieve the carousel effect. However, it is found that at this time, the entire View will be redrawn, resulting in huge overhead and unnecessary rendering. The current requirement is to modify a text, so there is no need to reload the entire Widget tree. Here we need to consider that the refresh range is not properly controlled. The improvement strategy is to encapsulate this component with carousel effect independently and implement the carousel effect in the same way; Widget build ( BuildContext context ) { In this way, the only widget rendered each time is the text component itself, as shown in the following figure: 2.3 Reduce the number of component redrawsDuring the development process, it is easy to trigger the re-rendering of the interface. Most of the time, the refresh times of the components are not controlled properly, which can easily lead to excessive memory consumption or multiple invalid network loading, resulting in problems such as freezing of the interface when sliding and poor user experience. As shown in the figure below, the interval selection effect is achieved with the help of the flutter_xlider third-party component: Process the interface and data refresh in the onDragCompleted callback method. The code is as follows: Widget rangeSliderView () { As shown in the figure above, there is a problem here. Selecting the same price range again will also trigger the interface and data refresh, which is a completely invalid refresh operation. The improvement strategy here is to add conditional restrictions to avoid repeated invalid refreshes. The optimized code is as follows: Widget rangeSliderView () { 2.4 Split ViewModel to reduce the probability of interface refreshIn the process of developing Flutter, you often don't use setState to control the state of an interface, because this will make the interface too fragmented and difficult to control. In this case, you can use Provider to manage the state of the interface, so that the state of the interface is centrally managed and the interface rendering is within a controllable range. The object that stores the state is called ViewModel. For a large interface, data may come from multiple sources. If all data and state values are stored in one ViewModel, the ViewModel will be too redundant. When the data in the ViewModel changes, the entire interface may be triggered to re-render, which is obviously inappropriate. Therefore, the ViewModel can be split, and one ViewModel can manage only one View as much as possible. The ViewModel is bound to the View, and then MultiProvider is used to store all Providers at the entrance of the interface, as shown below: MultiProvider ( A ViewModel only corresponds to one UI in the interface, that is, when the data changes, only the corresponding View will be controlled to refresh, and irrelevant Views will not be refreshed, thereby reducing the refresh frequency of irrelevant Views. 2.5 Caching High-Level ComponentsFor complex pages, each module at the page level is an independent component. All subcomponents are re-rendered every time the page is refreshed, which has a very high performance overhead. Try to reuse and avoid unnecessary view creation. List caches high-level components. ///Store all widgets on the interface for caching 2.6 const identifierWhen calling setState(), Flutter will rebuild every subcomponent in the current View. The way to avoid rebuilding all of them is to use const. Especially for some components with animation effects, const modifiers should be used to avoid frequent construction. At the same time, using const modifiers can also reduce garbage collection. 2.7 RepaintBinary IsolationFor some components that often need to be rendered, such as Swiper, PageView, Lottie, etc., you can use RepaintBoundary to isolate them. RepaintBoundary is the repaint boundary, and the user is independent of the parent layout when repainting. Because it provides a new layer for content that often changes in display, the new layer paint will not affect other layers. RepaintBoundary ( 2.8 Avoid using ClipPath componentsTry to avoid using ClipPath during development. Clipping path is a very expensive operation. When drawing widgets, ClipPath will affect each drawing instruction, perform intersection operations, and clip the remaining parts, so it is a time-consuming operation. If you just want to clip components such as rounded corners, it is recommended to use Container's raidus to set it. 2.9 Reduce the use of Opacity type componentsReduce the use of Opacity Widget, especially in animation, because it will cause the widget to be rebuilt every frame. You can use AnimatedOpacity or FadeInImage instead. AnimatedOpacity ( 3. Root Isoate Optimization3.1 Reduce logical processing in buildTry to reduce the processing logic in the build, because the widget will be rebuilt through the build at any time during the page refresh process. The build is called frequently and should only process logic related to the UI. Therefore, some operations that are not required for each rendering should be stored in initState, or variables should be used for state judgment to avoid a large number of repeated and unnecessary calculations every time the interface element refresh triggers the build redraw, thereby reducing CPU consumption. 3.2 Time-consuming calculations are put into Isolate for execution (multi-threading)For some time-consuming operations in the UI thread, you can use Isolate to execute them in a "multi-threaded" manner. Isolate is essentially closer to the concept of "process" in the operating system. There is no concurrent mechanism of shared memory in Dart. Since there is no need to worry about thread preemption, deadlock will not occur. Isolate has no shared memory, which is quite different from other common multi-threaded languages. Creating a thread will increase the memory by about 2MB. Try to avoid abusing it to cause memory overhead. The header of the hotel details page needs to calculate the current transparency in real time as the page scrolls. It is fully transparent when sliding to the top, and fully displayed when sliding out of the header image display area. In addition, the offset of each corresponding module needs to be monitored during the sliding process of the interface to modify the state of the top floating Tab. Therefore, isolate is used to isolate the logic of calculating transparency and offset in real time, and the result is returned after the calculation is successful. This will not affect the operation of scrolling the page in the UI main thread, and can improve the smoothness of the page. 4. Optimize the sliding performance of long lists4.1 ListView Item ReuseThrough GlobalKey, you can get the widget, including the renderBox of the component and other information about the element, and get the variables in the state. When a long list is loaded in pages, data changes will cause the entire ListView to be rebuilt. We can use globalkey to get the properties of the widget to achieve item reuse. This solves the page freeze problem caused by a large number of renderings after successful page loading. Widget listItem ( int index , dynamic model ) { Using GlobalKey should not rebuild GlobalKey every time you build it. It should be a long-lived object owned by State. 4.2 Home Page PreloadingIn order to reduce waiting time and allow users to see the content as soon as they enter the list page, preload the list data on the previous page. There are several cases for preloading data. If the data has been successfully loaded, it will be directly brought into the loading data result. "In-transit request" uses the bridge method to retrieve the data. The code is as follows: _loadHotels () { 4.3 Paging PreloadingNormally, the next page of data will be loaded only when the user scrolls to the bottom, so the user has to wait for the data to be loaded, which affects the user experience. You can use the remaining method to preload data. When the user scrolls to a certain number of hotels, the next page of data will be loaded. In the case of a good network, there is basically no waiting time for the interface to load when scrolling through the list interface. // getRectFromKey gets the location information of scrollView, traverses the specified number of remaining items, and loads the page data in the current screen 4.4 Canceling an in-progress network requestFrequent operations such as filtering will result in multiple network requests in a short period of time. If the network is poor or the server takes too long to return, data display will be disordered. When refreshing the list, you need to cancel the requests that have not yet returned data. _loadHotels () { 5. Image rendering performance and memory overhead managementImage loading is the most common and basic function of APP, and it is also one of the important factors affecting user experience. There are many technical details hidden behind the seemingly simple image loading. In the following chapters, we will mainly introduce some optimization attempts made on Flutter image loading. 5.1 Image loading principleTaking NetworkImage as an example, let's look at the image loading process in Flutter. First, the corresponding image resource is obtained through the resolve of ImageProvider to obtain ImageStream, which is decoded through the bottom layer and the texture is generated. ImageState receives the texture object to draw the image. After the upper layer obtains the image texture, it calls the SetState method of ImageState to pass the texture object to the bottom layer Render object. After the layout is completed, the image will be drawn to the screen. When the upper layer Image Widget is destroyed and the Image Cache is cleared, the release of the bottom layer texture is triggered. 5.2 Image loading managementIn business development, we always hope that the page content can be displayed to users as quickly as possible to give users a "straightforward" user experience. In the hotel list and detail pages, there are many pictures of hotels and room types. Too many pictures lead to high memory usage, time-consuming loading, and affect the user experience. 5.3 Image PreloadingData preloading: If the image resources used are some asynchronously acquired data, you can consider whether you can obtain the relevant data in advance and use it when you need it. Use idle resources to obtain the key data required for loading in advance. Image preloading mechanism: precacheImage, use precacheImage to preload the image data to be displayed into the memory in advance at the appropriate time. In this way, when the image is actually displayed, it has been loaded into the memory, and the "straight-out" effect can be achieved when the content is loaded. Delayed loading: In many scenarios, such as hotel lists and hotel detail head carousels, you only need to load the data on the first screen for the first time, and you can delay the loading of data outside the first screen to avoid instantaneous resource competition during loading, prioritize the loading of important resources, and achieve a good loading experience. 5.4 Image Resource OptimizationFor image resource processing and image compression, it is recommended to use the webp format first. Flutter natively supports the webp image format. CDN optimization is another very important aspect, mainly at the resource level, minimizing the size of transmitted images, responding to image requests as quickly as possible, optimizing image selection, supporting network image size cropping, and loading corresponding images according to actual needs, such as large header images and small thumbnails. According to specific scenarios, different image resources are loaded after cropping. 5.5 Image Memory OptimizationAfter preloading and resource optimization, related services can be loaded smoothly. However, loading too much data into the memory will cause excessive memory usage. How to use the memory reasonably and efficiently becomes the next problem to be solved. On the one hand, Flutter's image management capabilities are weak and lack local storage capabilities; on the other hand, when developing hybrid apps, due to the different caches mentioned above, repeated downloads of images can easily cause excessive memory usage, resulting in OOM (OutOfMemory) situations. After sorting out the Flutter native image solution, in order to provide a more stable and smooth experience, is there an opportunity to connect Flutter images and Native in a native way at some stage? Shared memory : Connect Native memory data to ensure that only one copy of the same data is kept in memory, avoiding memory overhead caused by repeated loading. Use disk cache to increase the amount of cached data. At the same time, Native and Flutter can share a copy of data through the disk, greatly reducing memory usage and ensuring smooth memory operation. Image loading: There are two ways to load images in Flutter: one is the default method that does not specify cacheWidth/cacheHeight. The final image is loaded using the original image resolution, which may lead to excessive memory usage and memory leaks; the other is to specify cacheWidth/cacheHeight to limit the loading resolution of the image. At the same time, the key of the image will also be affected. That is, loading the same source image multiple times with different resolutions will occupy memory multiple times, which is neither convenient nor saves memory. Therefore, in view of the above situation, the hit of the memory cache of the image is related to parameters such as width/height, cacheWidth/cacheHeight, etc. In this way, the cache data can be set according to the parameters of the image, which can more effectively ensure the authenticity and effectiveness of the cache. When using the cache, a problem is found that the image is easy to blur and deform. For example, when loading a high-definition large image, the sampling ratio cannot be simply calculated based on the width and height of the page widget. If it is set too small, it will be blurred, and if it is set too large, it will not be conducive to saving cache. VI. ConclusionThis article introduces how to use the Performance Overlay performance analysis tool to determine whether the problem is a performance problem with the UI thread or a performance problem with the GPU thread. The flame graph can be used to analyze which method caused the performance problem with the UI thread. The GPU thread problem can be determined by checking the number of renderings and the rendering range. The following are some of our commonly used performance optimization methods: UI thread optimization
GPU thread optimization
At the same time, we also introduced some experience optimization measures of Flutter in long lists and image loading. I hope it will be helpful for you to optimize Flutter performance and user experience. |
Recently, Australia announced at a conference tha...
Juyongguan is known as the most powerful pass in ...
Produced by: Science Popularization China Author:...
2015 has just begun, and the WeChat JS SDK was re...
How much does it cost to attack a server, and wha...
Late July to early August 2020 is the "launc...
We often see various commercial activities: Tmall...
Recently, the official of Shuangpai County, Hunan...
Many people want to become a programmer or switch...
[[124780]] Today, Tencent Big Data released the &...
1. Summary Recently, some colleagues who have jus...
If you like to use smart watches to track your ex...
As of today in 2020, with the rapid rise and matu...
Starts May 26 The "drama" of rising tem...