This paper introduces the React Native development process, how to select the infinite list components, how to use RecyclerListView components to optimize the performance of the infinite list, how to solve the problem of memory optimization and gesture overlap when using the infinite list and TAB. I hope it gives you some inspiration.
Background For products in the form of classified information flow, users switch between categories by swiping left and right, and browse more information by constantly swiping up.
Scan Up, Scan Up, Scroll Up, Show More List Items Scan Left, Scroll Left, Show Right List (blue)
Because React Native (RN) can meet the requirements of user experience, fast iteration, and cross-app development at a lower cost. Therefore, RN is used for product technology selection in the form of classified information flow. In the process of using RN to develop the home page, we have filled in a lot of pits, I hope these fills pit experience, have reference significance to readers. First, the performance of the official ListView/FlatList (RN) is poor, which has been ridiculed by the industry. Through the practice comparison, we choose the memory management efficiency of the third party component – RecyclerListView.
Second, the RecyclerListView needs to know the height of each list item in order to render correctly. What if the height of the list item is uncertain?
Third, there are some problems with the combination of tabs and infinite lists. First, how do you manage memory efficiently when you have multiple infinite lists in a TAB? Secondly, the TAB can scroll left and right, and the infinite list has left and right content components. When the gesture area of the two overlaps, how to assign the component priority?
Technical selection of lists
- ListView
In the process of developing products with the form of classified information flow in practice, we tried to use RN at the beginning, version 0.28. At the time, unlimited lists used the official ListView. List items in the ListView are never destroyed, which can cause memory to grow and cause a lag. The first 100 pieces of information scrolled very smoothly, 200 pieces of information began to slow down, to 1000 pieces of information basically did not move. At the time, there was no particularly good solution, only a compromise on the product that downgraded the infinite list to a limited list.
- FlatList FlatList is a new addition to RN 0.43 and has the memory collection feature, which can be used to implement an infinite list. We immediately followed up and upgraded the RN version. While Flatlist allows for an infinite list, the experience is always lacking. Flatlist works smoothly on iOS, but has a slight lag on some Android devices.
- RecyclerListView in practice development, we also try to use the RecyclerListView technology selection. RecyclerListView allows memory reuse and better performance. Both iOS and Android work smoothly.
The key measure of fluency is frame rate. The higher the frame rate, the more fluid it is, and the lower the frame rate, the more sluggish it is. We used RecyclerListView and FlatList, respectively, to achieve the same function of the unlimited list. We tested it on Android phones, and the scrolling frame rate is as follows.
Scrolling frame-rate comparison (Android Oppo R9 as an example)
RecyclerListView, FlatList, RecyclerListView are all RN list components. Why is the performance gap between them so large? We have done some research on the principle of its realization.
- ListView is implemented in a simple way. As the user slides up to load new list contents, the list items will be added continuously. Each new addition will cause memory to increase to a certain extent, the available memory space will not be enough, the page will be stalled.
- As a trick, since the user can only see what is on the screen of the phone, just render what the user sees (the viewable area) and what is about to be seen (near the viewable area). Where the user can’t see it (away from the visible area), delete it and fill it with blank elements. In this way, the memory of the blank area is freed. To achieve infinite loading, you must consider how to use memory efficiently. FlatList “remove one, add one” is one idea. RecyclerListView is another way of thinking.
- RecyclerListView RecyclerListView Assume that the types of items in the list are enumerable. All list items can be divided into several categories, for example, a picture layout is a category, two picture layout is a category, as long as the layout is similar is the same category of list items. Developers, need to declare the type in advance. Const types = {ONE_IMAGE: ‘ONE_IMAGE’, // const types = {ONE_IMAGE: ‘TWO_IMAGE’, // const types = {ONE_IMAGE: ‘TWO_IMAGE’,}
If the list item that the user is going to see is of the same type as the list item that the user is not going to see. Change the list that the user can’t see to the list item that the user is going to see. Modifications do not involve the overall structure of the component, only the parameters of the component’s properties, which usually include text, image addresses, and the location of the display. }
The < Text > a line of Text < / Text > < Image source = {{uri: '1. PNG}} / >
.
The < Text > a line of Text ~ ~ < / Text > < Image source = {{uri: '2. PNG}} / >
<View></View>
From the comparison of the three principles, we can find that the memory reuse of RecyclerListView is better than the memory recycling of FlatList, and the FlatList is better than the memory recycling of ListView.
Principle vs. gesture scrolling up, page scrolling up, loading more list items (dark green)
RecyclerListView RecyclerListView reuse the position of list items that need to change frequently, so absolute positioning is used, not the Flex layout from top to bottom. With absolute positioning, you need to know the position of the list item (top). For the user’s convenience, the RecyclerListView lets the developer pass in the height of all list items, and the internal recyclerListView automatically calculates their position.
- In the simplest case, the height of all list items is known. You can have the RecyclerListView state by combining the height and type data with the Server data. Const types = {ONE_IMAGE: ‘ONE_IMAGE’, // const types = {ONE_IMAGE: ‘TWO_IMAGE’, // const types = {ONE_IMAGE: ‘TWO_IMAGE’,}
// server data
const serverData = [
{ img: ['xx'], text: '' },
{ img: ['xx', 'xx'], text: '' },
{ img: ['xx', 'xx'], text: '' },
{ img: ['xx'], text: '' },
]
// RecyclerListView state
const list = serverData.map(item => {
Switch (item.img.length) {case 1: // 100px return {... item, height: 100, type: types.ONE_IMAGE, } case 2: return { ... item, height: 100, type: types.TWO_IMAGE, } default: return null }
})
- The height of a list item is not determined for all list items. For example, in the list item below above, the height of the image is determined, but the height of the text is determined by the length of the text sent by the Server. The text may be one line, two lines, many lines, and a few lines of text are indeterminate, so the height of the list item is indeterminate. How should we use the RecyclerListView component?
2.1 Native asynchronous access to highly Native end, in fact, there is an API – FontMetrics – to calculate the height of text in advance. By exposing the Native FontMetrics API to JS, JS will have the ability to calculate height in advance. The RecyclerListView needs to have the state computed as follows. The value is of type Promise. const list = serverData.map(async item => {
switch (item.img.length) { case 1: return { ... item, height: await fontMetrics(item.text), type: types.ONE_IMAGE, } case 2: return { ... item, height: await fontMetrics(item.text), type: types.TWO_IMAGE, } default: return null }
})
Each call to FontMetrics requires an asynchronous communication between OC/Java and JS. Asynchronous communication is very time consuming, and this scheme will significantly increase the rendering time. In addition, the new FontMetrics interface, which relies on the Native release, can only be used in the new version, not the old version. Therefore, we did not adopt. 2.2 position correction of open RecyclerListView forceNonDeterministicRendering = true attribute, can automatically correct layout position. The idea is that the developer estimates the height of the items in the list, and the RecyclerListView renders the view at that height. When the view is rendered, get the actual height of the list items through OnLayout, and then animate the view to move it to the correct position.
The position correction scheme is very suitable for the scene with small estimation deviation, but in the scene with large estimation deviation, obvious overlap and displacement phenomena can be observed. So, is there a way to estimate things with little bias and less time? In most cases, the uncertainty of the height of a list item is caused by the uncertainty of the text length. Therefore, as long as you can approximate the height of the text. 1 Chinese character with a width of 17px and a height of 20px, rendered with a width of 17px and a height of 20px. If the container width is wide enough and the text is not broken, the width of the 30 characters will be 30, 17px = 510px, and the height will still be 20px. If the container is only 414px wide, then it will obviously break into two lines, with the height of the text at 220px =40px. The general formula is: Line Number = Math.ceil Text Height = Line Number * Line Height In fact, there are not only Chinese characters, but also lowercase letters, uppercase letters, numbers, Spaces, and so on. In addition, there are different render font sizes. As a result, the resulting line count algorithm is also more complex. We have run a variety of real machine tests to get flat render widths for all types of characters at 17px, such as 11px for uppercase letters, 8.6px for lowercase letters, etc. The algorithm summary is as follows: /**
- @param STR string text
- @ param fontSize font size
-
Returns */ function getStrWidth(STR, fontSize) {const scale = fontSize / 17; const capitalWidth = 11 * scale; // const lowerWidth = 0.10 * scale; // const spaceWidth = 4 * scale; // void const numberWidth = 9.9 * scale; // const ChineseWidth = 17.3 * scale; // Chinese and others
const width = Array.from(str).reduce(
(sum, char) => sum + getCharWidth(char, { capitalWidth, lowerWidth, spaceWidth, numberWidth, chineseWidth, }), 0,
);
return Math.floor(width / fontSize) * fontSize;
}
/ * *
- @param string string text
- @param fontSize fontSize
- @param width renders the container width
- Return (String, fontSize, width) {return Math. Ceil (String, string); return Math. fontSize) / width); } The above pure JS algorithm for estimating the number of lines of text has a measured accuracy of about 90% and an estimated time of milliseconds, which can well meet our needs. Therefore, our final solution is to estimate the number of lines of text through JS, get the height of the text, and then further infer the layout height of the list items. And open forceNonDeterministicRendering = true, in estimating deviation, automatic animation fixed the position of the list item. For products in the form of classified information flow, some may contain multiple tabs, each with a specific content, and most of these tabs are infinite lists. If the contents of all the tabs are present at the same time, memory is not freed, which can also cause performance problems.
- Memory collection follows the previous approach to dealing with list memory. We can choose between memory collection and memory reuse. The premise of memory reuse is that the structure of the reused content is the same, and only the data changes. In actual business, products have classified similar content, and each TAB page has its own characteristics, making it difficult to reuse. Therefore, for TAB pages, memory collection is a better choice. The whole idea is that tabs in the visible area must be displayed. The most recently displayed content in the viewable area is retained as appropriate. Content far from the visible area needs to be destroyed.
Destroy tabs away from the visible area
- TabView 1.0 uses RN’s own gesture system, which works well when you swipe left and right to switch tabs. If in the viewable area, there are both tabs that can scroll left and right, and content areas that can scroll left and right. When the user scrolls left over the gesture overlap, does the TAB respond to scroll, or does the content area respond, or both?
Gesture overlaps area, scroll left, who responds? Because RN gesture recognition is done simultaneously in the OC/Java render main thread and in the JS thread, this strange processing method makes the gesture difficult to be handled accurately. As a result, TabView 1.0 does not handle business scenarios with overlapping gestures well. In TabView 2.0, a new Gesture system, the React Native Gesture Handler, is integrated. The new gesture system is a declarative gesture system that is handled by the pure OC/Java render main thread. We can pre-declare the gesture response in our JS code and have the TAB waitFor the gesture response in the content area. In other words, the overlapping area gesture only applies to the content area.
This article describes some of the common problems we encountered when developing an infinite list of products in the form of classified information flow using RN, and how to consider, optimize, and select them technically. I hope it can be used for reference.