Skip to content

Latest commit

 

History

History
466 lines (308 loc) · 26.4 KB

All-ListView-and-Virtualization-Optimization.md

File metadata and controls

466 lines (308 loc) · 26.4 KB

React Native — Ultimate Guide on Virtualization Performance Optimization

Optimize list performance — FlatList, SectionList, VirtualizedList, ScrollView (All in one) 🔥

What is Virtualization?

In this guide, we will discuss list virtualization (also known as windowing). This is the idea of rendering only visible rows of content in a dynamic list instead of the entire list. The rows rendered are only a small subset of the full list with what is visible (the window) moving as the user scrolls. This can improve rendering performance.

Virtualization massively improves memory consumption and performance of large lists by maintaining a finite render window of active items and replacing all items outside of the render window with appropriately sized blank space.

Virtualization Components in React Native

Mainly, these are virtualization components provided by the React Native team.

  1. VirtualizedList

  2. FlatList

  3. SectionList

  4. ScrollView with Virtualization (Experimental & not suggested)

We will see each of these implementations in code and then do performance optimization on each of them so that they can all load more than 10,000 items.

Native RCTScrollView Component

Before starting with the React Native virtualization component, let’s understand the Native Wrapper of all virtualization components in React Native.

“RCTScrollView” is a native component that is used by the ScrollView component in React Native. It is a wrapper around the native UIScrollView on iOS and the native android.widget.ScrollView on Android. It provides the functionality of scrolling a view that contains multiple subviews or a large content that does not fit the screen. It also supports features such as bouncing, paging, horizontal mode, sticky headers, and scroll indicators.

The relation between “RCTScrollView” and “ScrollView” is that “ScrollView” is a React Native component that renders a “RCTScrollView” under the hood. You can use the “ScrollView” component in your React Native code to render a scrollable view with your custom content. You do not need to import or use the “RCTScrollView” component directly, as it is handled internally by the “ScrollView” component.

The relation between “RCTScrollView” and other list components such as FlatList, SectionList, and RecyclerListView is that they are all based on the same native component (“RCTScrollView”) that provides the scrolling functionality for the list views. However, they have different implementations and features that make them suitable for different use cases and scenarios.

VirtualizedList

Let’s deep dive into the implementation of VirtualizedList. Here is the basic implementation for VirtualizedList.

Basic implementation for VirtualizedList

And below is the function ‘renderItem’ to render the JSX elements for VirtualizedList.

Output (IOS + Android)

Basic VirtualizedList view

Optimize the performance of VirtualizedList

Let’s add some new props to the VirtualizedList component to improve the performance so that it can render a list of 10000+ items.

Added new props to VirtualizedList to improve its rendering performance.

Understand the props of VirtualizedList

Let’s understand some props that you can pass to the VirtualizedList component to improve the rendering performance.

  • initialNumToRender: How many items to render in the initial batch. This should be enough to fill the screen but not much more. Note these items will never be unmounted as part of the windowed rendering in order to improve perceived performance of scroll-to-top actions. Default value = 10.

  • removeClippedSubviews: This may improve scroll performance for large lists. Note: May have bugs (missing content) in some circumstances — use at your own risk.

  • windowSize: Determines the maximum number of items rendered outside of the visible area, in units of visible lengths. So, if your list fills the screen, then windowSize={21} (the default) will render the visible screen area plus up to 10 screens above and 10 below the viewport. Reducing this number will reduce memory consumption and may improve performance but will increase the chance that fast scrolling may reveal momentary blank areas of unrendered content.

  • keyExtractor: Use the keyExtractor prop to provide a unique key for each item in the data array. This helps React to identify which items have changed, added, or removed. The key should be a string that is stable and does not change over time.

  • getItemLayout: Use the getItemLayout prop to specify the height (or width, for horizontal lists) of each item in pixels. This allows the VirtualizedList to skip measurement of the items and optimize the scrolling performance. The function should return an object with length, offset, and index properties.

  • maxToRenderPerBatch: Use the maxToRenderPerBatch prop to control how many items are rendered in each batch after the initial render. The larger this number, the less often the user will see blank space while scrolling, but the more work the UI thread has to do.

  • updateCellsBatchingPeriod: Use the updateCellsBatchingPeriod prop to control how often the VirtualizedList attempts to update its cells. The default value is 50 milliseconds, which means that at most one update per 50 milliseconds will be performed. Increasing this number can reduce CPU usage and improve performance but may cause more lag in updating the list.

  • removeClippedSubviews: This may improve scroll performance for large lists. On Android the default value is true. (Note: May have bugs (missing content) in some circumstances — use at your own risk.)

Some challenges I faced while implementing VirtualizedList

Since VirtualizedList is very customizable and the base implementation for virtualization in React Native, you need to handle some basic needs in your own implementation logic.

#Challange_1

I found that creating multiple columns in VirtualizedList is quite complex and tough to handle. I created a 2-column view by using the code below. Therefore, I needed to add the prop getItem like below.

VirtualizedList Implementation

Here is the code for the function that implements the logic for showing 2 columns.

getItem props function implementation

Now that you have added the getItem prop with the 2-column logic, you need to change the child component behavior for rendering the 2-column view. Here is the code for rendering the 2-column view for the VirtualizedList component.

VirtualizedList child component JSX code for render the view

So, in the child component of VirtualizedList, we see that we needed to add an extra map function. Therefore, every time this item array length will be 2 as I created a 2-column based logic in the getItem prop function. This was a huge change in the code for showing a 2 or multi-column view in the VirtualizedList component.

#Challenge_2:

Another challenge I faced to use getItemLayout. From the previous of my explanation on getItemLayout you already understand that how it works. Basically you need to set length, offset, index.

Where,

  1. length means: the height (or width, if horizontal) of your item, in pixels.

  2. offset means: the distance (in pixels) of the current item from the top of the list. It helps the list to calculate the scroll position and render only the visible items on the screen. The easiest way to calculate the offset for items with constant height is to multiply the height by the index, which gives the position immediately after the previous item. For example, if each item has a height of 50 pixels, then the offset for the third item is 50 * 2 = 100 pixels.

  3. index means: The index is the position of your item in the data array, starting from 0.

So, to set it, I applied this code but I observed that it was not set properly and it made my VirtualizedList view kind of shaky and inappropriate in behavior.

getItemLayout props function

And this is how I set getItemLayout in VirtualizedList.

set getItemLayout props in VirtualizedList

I skipped getItemLayout in my VirtualizedList implementation as already my VirtualizedList implementation works smoothly with 1000+ image Items.

Final code of VirtualizedList Implementation

Here is the final code of VirtualizedList implementation with both

  1. Highly optimized the performance for rendering 1000+ Items.

  2. Implemented multi column VirtualizedList.

    // Virtualized List Render JSX element 👇
    function renderVirtualizedListItem({ item, index }) {
        return (
            <View key={index} style={styles.virtualizedListStyle}>
                {item.map((elem, i) => (
                    <View key={i}>
                        <Image
                            style={styles.image}
                            resizeMethod="resize"
                            source={{
                                uri: elem.easyImageUrl
                            }}
                        />
                        <Text>{elem.nftName}</Text>
                    </View>
                ))}
            </View>
        )
    }
    
    // Define how ROW of items will be visible 👇
    function getTwoColumnVirtualizedListItems(data, index) {
        // Return two items per row
        let items = []
        for (let i = 0; i < 2; i++) {
            const item = data[index * 2 + i]
            item && items.push(item)
        }
        return items
    }
    
    return (
        {/* VirtualizedList 🔥 🔥 🔥  */}
        <VirtualizedList
            data={myNftsData.findMyNfts}
            renderItem={renderVirtualizedListItem}
            getItemCount={(data) => data.length}
            getItem={getTwoColumnVirtualizedListItems}
            initialNumToRender={10}
            maxToRenderPerBatch={10}
            windowSize={11}
            updateCellsBatchingPeriod={100}
        />
    )
    

VirtualizedList Final Output (IOS + Android)

1000+ Images rendering using VirtualizedList

FlatList

Let’s implement the above same data for FlatList

FlatList implementation

Red marked props were used for performance optimization. These are all the same as the props of VirtualizedList. I have already details explained about all these options in VirtualizedList section.

Now, below is the code for renderItem props function implementation for rendering JSX code.

renderItem props function implementation

Final code for FlatList

function renderFlatlistItem({ item }) {
  return (
   <View
    style={styles.nftCard}>
    <Image
     style={styles.image}
     resizeMethod="resize"
     source={{
      uri: item.url
     }}
    />
    <Text>{item.nftName}</Text>
   </View>
  )
 }

return (
    {/* FlatList 🔥 🔥 🔥  */}
    <FlatList
       data={myNftsData.findMyNfts}
       renderItem={renderFlatlistItem}
       keyExtractor={(item) => item._id}
       numColumns={2}
       removeClippedSubviews={true}
       maxToRenderPerBatch={10}
       updateCellsBatchingPeriod={100}
       initialNumToRender={10}
       windowSize={11}
    />
)

Comparative analysis between VirtualizedList & FlatList

There is not much difference in performance between VirtualizedList and FlatList, as they are both based on the same base implementation. However, some factors that may affect the performance are:

  • The type and size of the data array: VirtualizedList can work with immutable data instead of plain arrays, which may be more efficient for some use cases. However, FlatList may have better memory management for large arrays.

  • The layout and complexity of the items: VirtualizedList allows more flexibility and customization of the item layout, which may be useful for complex or dynamic items. However, FlatList provides some convenient features, such as numColumns, horizontal, and inverted, which may simplify the layout for some scenarios.

  • The configuration and optimization of the props: Both VirtualizedList and FlatList have several props that can help to improve the performance, such as removeClippedSubviews, maxToRenderPerBatch, updateCellsBatchingPeriod, initialNumToRender, windowSize, getItemLayout, and keyExtractor. These props should be adjusted according to the needs and preferences of each case.

    In general, FlatList is recommended for rendering basic, flat lists with simple data and layout. VirtualizedList is recommended for rendering complex lists with custom data and layout.

SectionList

Let’s implement the above same data with SectionList. As, now we are going to show some section wise List, so we will add 2 sections in our List.

  1. Male Players

  2. Female Players

Let’s see the code of SectionList component

SectionList component code

Now let’s see each function props of SectionList

Props functions of SectionList component

View of SectionList

SectionList output

MultiColumn view in SectionList

Let’s see now how we can make multiple column in SectionList. Basically there are 2 ways in how we can apply multi column view in SectionList.

#First_Way

✅ Apply FlatList inside SectionList renderItem props function.

Let’s see the code of first way.

Apply FlatList inside SectionList

Now add the function renderSection into your SectionList component like below

Added renderSection function in renderItem props

The demerits of this way are:

  • You may encounter some performance issues and warnings, such as “VirtualizedLists should never be nested inside plain ScrollViews” or “Invariant Violation: Changing numColumns on the fly is not supported”.

  • You may have to deal with some complexity and bugs, such as missing content, duplicate rendering, scrolling behavior, etc.

#Second_Way

✅ The second way is a bit more complex than the previous way. It is like an algorithmic way to maintain the performance of SectionList as good as the original performance.

You just need to change the renderItem props function like below:

Algorithmic way for showing Multi line column in SectionList

This above function is for the renderItem props functions. Just replace your necessary code in the above function & send the function as renderItem props function of SectionList.

❝ If you like my Algorithmic way, please give a STAR 🙏

Multi Column view of SectionList

Final code of SectionList

Let me show you the code of SectionList with multi-column and performance optimization:

function sectionsData() {
  return [
   {
    title: 'Male Players',
    data: myNftsData.findMyNfts.filter(
     (item) => item.collectionType == 'Male'
    )
   },
   {
    title: 'Female Players',
    data: myNftsData.findMyNfts.filter(
     (item) => item.collectionType == 'Female'
    )
   }
  ]
 }


 function renderItem({ section, index }) {
  const numColumns = 2 // Show 2 columns

  // Return null as ITEM already PUSHED into "items" array
  if (index % numColumns !== 0) return null

  // "numColumns" row wise ITEMS will be inserted into this Array
  const items = []

  // Loop for "numColumns" times for each valid "index"
  for (let i = index; i < index + numColumns; i++) {
   if (i >= section.data.length) {
    break
   }

   // PUSH row wise elements into "items" array. As i set "numColumns" as 2 so the final view 👇
   // [Item index: 0, Item index: 1]
   // [Item index: 2, Item index: 3]
   // [Item index: 4, Item index: 5]
   items.push(
    <View style={styles.nftCard}>
     <Image
      style={styles.image}
      resizeMethod="resize"
      source={{
       uri: section.data[i].url
      }}
     />
     <Text>{section.data[i].nftName}</Text>
    </View>
   )
  }

  // When index will be 0 it will return 👉 (<View> [Item index: 0, Item index: 1] </View>)
  // When index will be 2 it will return 👉 (<View> [Item index: 2, Item index: 3] </View>)
  // When index will be 4 it will return 👉 (<View> [Item index: 4, Item index: 5] </View>)
  return (
   <View
    style={{
     flexDirection: 'row',
     justifyContent: 'space-between'
    }}>
    {items}
   </View>
  )
 }


 function renderSectionHeader({ section }) {
  return (
   <View style={{ backgroundColor: '#eee', padding: 10 }}>
    <Text>{section.title}</Text>
   </View>
  )
 }


return (
  <SectionList
       sections={sectionsData()}
       renderItem={renderItem}
       renderSectionHeader={renderSectionHeader}
       keyExtractor={(item) => item._id}
       // Performance props
       maxToRenderPerBatch={20} // Increase the number of items per batch
       updateCellsBatchingPeriod={100} // Increase the time interval between updates
       windowSize={11} // Reduce the number of screens to render
       removeClippedSubviews={true} // Remove offscreen views
    />
)

ScrollView

Component that wraps platform ScrollView while providing integration with touch locking “responder” system.

Keep in mind that ScrollViews must have a bounded height in order to work, since they contain unbounded-height children into a bounded container (via a scroll interaction). In order to bound the height of a ScrollView, either set the height of the view directly (discouraged) or make sure all parent views have bounded height. Forgetting to transfer {flex: 1} down the view stack can lead to errors here, which the element inspector makes quick to debug.

Doesn’t yet support other contained responders from blocking this scroll view from becoming the responder.

Load 1000+ images with ScrollView

ScrollView is not for rendering lists. ScrollView is only for rendering views that are scrollable. However, let’s see how we can use ScrollView to render a list.

ScrollView implementation for rendering a List

You can see that I applied “removeClippedSubviews” as true. It will help to render a list. Let’s see what this “removeClippedSubviews” do in ScrollView.

“removeClippedSubviews”: It is still experimental. When true, offscreen child views (whose overflow value is hidden) are removed from their native backing superview when offscreen. This can improve scrolling performance on long lists.

Final code for ScrollView

<ScrollView
 removeClippedSubviews={true}
 showsVerticalScrollIndicator={false}>
 <View key={nftCollectionType}>
  {myNftsData.findMyNfts.map((item, index) => (
   <View key={index}>
    <Image
     style={styles.image}
     resizeMethod="resize"
     source={{
      uri: item.url
     }}
    />
    <Text>{item.nftName}</Text>
   </View>
  ))}
 </View>
</ScrollView>

Observation

For my case, I was able to load more than 200 images easily by using it, but I suggest that if you need to render a list, then use any list-supported component like FlatList, VirtualizedList, etc.

ScrollView vs Virtualization

Let’s see some comparative analysis:

  • ScrollView renders all its child components at once, which can cause slow rendering and increased memory usage if the list is large. FlatList, on the other hand, renders items lazily, only when they are about to appear on the screen, and removes items that scroll way off screen to save memory and processing time.

  • ScrollView does not have built-in optimizations for rendering and updating large lists, such as separators, multiple columns, infinite scroll loading, etc. FlatList supports these features out of the box.

  • ScrollView maintains the state of its child components, which can be useful for some scenarios, but also adds overhead to the rendering process. FlatList unmounts and recreates components from scratch when they scroll in and out of view, which can improve performance but also lose state.

When to use ScrollView & When to use FlatList/ VirtualizedList?

ScrollView and FlatList are two components that can render a list of items in React Native. However, they have different ways of handling state. ScrollView maintains the state of its child components, which means that it remembers the data and UI changes of each item in the list. FlatList unmounts and recreates components from scratch when they scroll in and out of view, which means that it does not preserve the data and UI changes of each item in the list.

To illustrate this difference, let’s look at an example. Suppose you have a list of 100 items that can be selected by tapping on them. Each item has a text and a checkbox to indicate its selection status. You can use either ScrollView or FlatList to render this list, but the result will be different.

If you use ScrollView, you will be able to select and deselect any item in the list, and the checkbox will reflect the selection status correctly. This is because ScrollView keeps track of the state of each item and renders them accordingly.

If you use FlatList, you will be able to select and deselect any item in the list, but the checkbox may not reflect the selection status correctly. This is because FlatList does not keep track of the state of each item and renders them from scratch when they scroll in and out of view. For example, if you select an item at the top of the list and then scroll down to the bottom, the item may appear as unselected when you scroll back up. This is because FlatList has unmounted and recreated the item component without preserving its state.

Therefore, if you need to preserve the state of each item in a large list, ScrollView may be more suitable than FlatList. However, ScrollView has other drawbacks such as slow rendering and increased memory usage, so you need to weigh the pros and cons carefully.

Nested Virtualization Error

If you see this error anytime in your metro 👇

Error: ❝ VirtualizedLists should never be nested inside plain ScrollViews ❞

Then here is the solution article with error reason & analysis: React Native — Nested Virtualization Anti-pattern (Performance Optimization)

What Next ?

Since you have mastered React Native Virtualization, you are ready to explore FlashList, which is a new approach that uses ‘Cell Recycling’ instead of Virtualization. I will guide you through the steps and explain the terms related to FlashList and Cell Recycling. I also describe my journey on how I loaded 64,000 image Items in FlashList without any lac.

FlashList article link: React Native — FlashList: Performant List view (Implementation + Analysis)

Thank you for reading this article. I enjoy sharing my 5 years of experience in React-native, JavaScript, React & Node.js with you every day. If you enjoyed reading this article, I would appreciate it if you could follow me on Twitter & Medium.

If you find any ISSUE in this Guide BOOK, please create a PR to help the community 🔥