2

Lead
This article introduces how to select the technical selection of the infinite list component in the development process of React Native, how to use the RecyclerListView component to optimize the performance of the infinite list, and how to solve the problem of memory optimization and gesture overlap when the infinite list is used with tabs. , I hope to inspire everyone.

background
For products in the form of classified information flow, users swipe left and right to switch classifications, and browse more information by swiping up continuously.

WeChata137821f0492646a08d5fd92e99479bf.png

Use tabs (Tabs) to switch categories, and use infinite lists (List) to slide up and browse
Swipe up by gesture, scroll up the page, show more list items (List Item)
Gesture swipe left, the page scrolls to the left, showing the list on the right (blue)

Because React Native (RN) can use a lower cost, while meeting the user experience, rapid iteration, and cross-app development and online requirements. Therefore, RN is used for product technology selection in the form of classified information flow. In the process of using RN to develop the homepage, we have filled in many pits, and hope that these pit-filling experiences will be useful for readers.
First, the performance of the infinite list (ListView/FlatList) officially provided by RN is too poor and has been complained by the industry. Through practical comparison, we chose a third-party component with better memory management efficiency-RecyclerListView.

Second, 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, when using a combination of tabs and unlimited lists, you will encounter some problems. First of all, there are multiple infinite lists in the tab page, how to effectively manage memory? Secondly, the tabs can slide left and right, and there are content components that scroll left and right in the infinite list. When the two gesture areas overlap, how to specify the priority of the components?

Technical selection of the list

  1. ListView

In the process of developing products in the form of classified information flow, we began to try to use RN, the version is 0.28. At that time, the infinite list used the official ListView. ListView's list items will never be destroyed, which will cause the memory to continue to increase, leading to stuttering. The first 100 messages scrolled very smoothly. It started to freeze after 200 messages, and basically stopped sliding after 1000 messages. At that time, there was no particularly good solution, only to compromise on the product, downgrading the unlimited list to a limited list.

  1. FlatList
    FlatList was added in version 0.43 of RN. It has the function of memory recycling and can be used to implement unlimited lists. We followed up immediately and upgraded the RN version. Although FlatList can implement an infinite list, the experience is still lacking. FlatList performs very smoothly on iOS, but there will be a slight lag on some Android models.
  2. RecyclerListView
    In practice and development, our technology selection also tried to adopt RecyclerListView. RecyclerListView realizes the reuse of memory, and the performance is better. Both iOS and Android performed very smoothly.

Fluency comparison
The key indicator to measure fluency is the frame rate. The higher the frame rate, the smoother, and the lower the frame rate, the more lag. We used RecyclerListView and FlatList to implement an infinite list of the same function, respectively, and tested it on an Android phone. The scrolling frame rate is as follows.

WeChat647a25498ff0426b6f4a4229fad9b67b.png

Scrolling frame rate comparison (taking Android OPPO R9 as an example)
WeChat5e918078079759f4856cb3cca5d94973.png

Implementation principle comparison
ListView, FlatList, RecyclerListView are all list components of RN, why is there such a big performance gap between them? We did some research on its realization principle.

  1. ListView
    The implementation idea of ListView is relatively simple. When the user slides up to load new list content, list items are continuously added. Each new addition will cause the memory to increase. After increasing to a certain extent, the usable memory space is insufficient, and the page will be stuck.
  2. FlatList
    FlatList takes a trick. Since the user can only see the content on the mobile phone screen, only the part that the user sees (the visible area) and the part that is about to be seen (near the visible area) is rendered. And the place that the user can't see (away from the visible area), just delete it, and use a blank element to occupy the place. In this way, the memory in the blank area is released.
    To achieve unlimited loading, we must consider how to efficiently use memory. FlatList "delete one, add one" is an idea. RecyclerListView "Similar structure, change and reuse" is another idea.
  3. RecyclerListView
    RecyclerListView assumes that the types of list items are enumerable. All list items can be divided into several categories. For example, the graphic layout of a picture is one type, and the graphic layout of two pictures is the same type. As long as the layouts are similar, the list items of the same type are listed. Developers need to declare the type in advance.
    const types = {
    ONE_IMAGE:'ONE_IMAGE', // The graphic layout of a picture
    TWO_IMAGE:'TWO_IMAGE' // Graphic layout of two pictures
    }

If the list item that the user is about to see is the same type as the list item that the user cannot see. Just change the list that the user can't see to the list item that the user will see soon. The modification does not involve the overall structure of the component, only the attribute parameters of the component, usually including text, image address, and display location.
{/ /}
<View style={{position: 'absolute', top: disappeared}}>

<Text>一行文本</Text>
<Image source={{uri: '1.png'}}/>

<View>
{/ modified to the list item that the user will see soon /}
<View style={{position: 'absolute', top: visible}}>

<Text>一行文本~~</Text>
<Image source={{uri: '2.png'}}/>

<View></View>

From the comparison of the three principles, we can find that in terms of memory efficiency, the memory reused RecyclerListView is better than the memory recovery FlatList, and the FlatList is better than the memory non-recycling ListView.

Principle comparison
Swipe up by gesture, scroll up the page, load more list items (dark green)

Practice of RecyclerListView
The position of RecyclerListView reuse list items needs to change frequently, so the absolute positioning position: absolute layout is used instead of the flex layout from top to bottom. With absolute positioning, you need to know the position of the list item (top). For the convenience of users, RecyclerListView allows developers to pass in the height (height) of all list items, and its position (top) is automatically inferred internally.

  1. Highly determined list items
    In the simplest example, the heights of all list items are known. Just merge the height, type data, and Server data to get the state of the RecyclerListView.
    const types = {
    ONE_IMAGE:'ONE_IMAGE', // The graphic layout of a picture
    TWO_IMAGE:'TWO_IMAGE' // Graphic layout of two pictures
    }

// 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
}

})

  1. Highly uncertain list items
    Not all list items have a certain height. For example, in the list item in the figure below, although the height of the picture is determined, 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, or multiple lines. The number of lines in the text is uncertain, so the height of the list item is also uncertain. So, how should the RecyclerListView component be used?

WeChat046c9273fdb883dc918b2929dbd06fe0.png

2.1 Native obtains height asynchronously
On the Native side, there is actually an API that calculates the height of the text in advance-fontMetrics. Expose the Native fontMetrics API to JS, and JS does not have the ability to calculate the height in advance. At this point, the state calculation method required by RecyclerListView is as follows, and its value is of the promise type.
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
}

})

Every time fontMetrics is called, an asynchronous communication between oc/java and js is required. Asynchronous communication is very time-consuming, and this solution will significantly increase the time-consuming rendering. In addition, the new fontMetrics interface solution relies on the Native release and can only be used in the new version, not the old version. Therefore, we did not adopt it.
2.2 Position correction
After enabling the forceNonDeterministicRendering=true property of RecyclerListView, the layout position will be corrected automatically. The principle is that the developer estimates the height of the list item in advance, and the RecyclerListView first renders the view according to the estimated height. After the view is rendered, get the true height of the list item through onLayout, and then move the view to the correct position through animation.

WeChatc77458f6a6135fbe449d8f1d680f9d78.png

Position correction
This scheme is very suitable for scenarios with small estimated height deviations, but in scenarios with large estimated deviations, obvious overlap and displacement will be observed. So, is there a method with small estimation bias, time-consuming and short-term method?
2.3 JS estimated height
In most cases, the uncertainty of the height of the list item is caused by the uncertainty of the text length. Therefore, as long as the height of the text can be roughly estimated.
A Chinese character with 17px font size and 20px line height will be rendered with a width of 17px and a height of 20px. If the container width is wide enough, the text does not wrap, and 30 Chinese characters, the rendered width is 30 17px = 510px, and the height is still 20px. If the width of the container is only 414px, it will obviously be folded into 2 lines, and the text height is 2 20px =40px. The general formula is:
Number of lines = Math.ceil (text non-folding width / container width)
Text height = number of lines * line height
In fact, the character types include not only Chinese characters, but also lowercase letters, uppercase letters, numbers, spaces, etc. In addition, the rendered font size is also different. Therefore, the final text line count algorithm is also more complicated. Through a variety of real machine tests, we have obtained the draw width of various character types under 17px, such as uppercase letters 11px, lowercase letters 8.6px, etc. The algorithm summary is as follows:
/**

  • @param str string text
  • @param fontSize font size
  • @returns non-folding width
    */
    function getStrWidth(str, fontSize) {
    const scale = fontSize / 17;
    const capitalWidth = 11 * scale; // capital letters
    const lowerWidth = 8.6 * scale; // lowercase letters
    const spaceWidth = 4 * scale; // space
    const numberWidth = 9.9 * scale; // number
    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 font size
  • @param width render container width
  • @returns number of rows
    */
    function getNumberOfLines(string, fontSize, width) {
    return Math.ceil(getStrWidth(string, fontSize) / width);
    }
    The above-mentioned pure js algorithm for estimating the number of text lines has an actual measurement accuracy of about 90%, and the estimation time is milliseconds, which can well meet our needs.
    2.4 JS estimated height + position correction
    Therefore, our final plan is to estimate the number of text lines through JS, and get the text height, and then further infer the layout height of the list item. And turn on forceNonDeterministicRendering=true, when there is a deviation in the estimation, the position of the list item will be automatically corrected by animation.
    WeChat6e4003adc60f86aa2ca9ecc5bf076c0b.png
    Unlimited list in tabs
    For products in the form of classified information flow, some will contain diversified tags, each of which has specific content, and most of the tab pages are unlimited lists. If the contents of all tabs exist at the same time, the memory will not be released, which will also cause performance problems.
  1. Memory reclamation
    Following the idea of dealing with list memory above, we can choose memory reclamation or 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, which makes it difficult to reuse. Therefore, for tab pages, memory reclamation is a better choice.
    The overall idea is that the tabs in the visible area must be displayed. The content recently displayed in the visible area will be retained according to the situation. The content far away from the visible area needs to be destroyed.

WeChate45fe1445de2151ad2b376d8568dfeac.png

Destroy tabs away from the visible area

  1. Handling of overlapping gestures
    The tab TabView 1.0 uses the RN's own gesture system. With a separate left and right sliding tab, the built-in gesture system works well. If in the visible area, there are tabs that can be swiped left and right, and there are content areas that can be scrolled left and right. When the user scrolls the gesture overlap area to the left, does the tab page respond to scrolling, or the content area responds, or does it respond at the same time?
    WeChatab2910f96f775ad392926a3410839f39.png

Gesture overlap area, scroll to the left, who responds?
Because RN's gesture recognition is performed simultaneously in the oc/java rendering main thread and the js thread, this strange processing method makes it difficult to accurately process the gestures. This results in TabView 1.0 not being able to handle business scenarios where gestures overlap.
In TabView 2.0, the new gesture system React Native Gesture Handler is integrated. The new gesture system is declarative, a gesture system handled by the main thread of pure oc/java rendering. We can pre-declare the gesture response method in the JS code, and let the tab page wait for (waitFor) the gesture response in the content area. In other words, the overlapping area gestures only act on the content area.

to sum up
This article introduces some common problems we encountered in the infinite list of products that use RN to develop classified information flow forms, and how to make technical considerations, optimizations and choices. I hope it can be useful for everyone.


fitfish
1.6k 声望950 粉丝

前端第七年,写一个 RN 专栏。