31

I'm experiencing some out of memory crashes in production. Trying to isolate the problem I could make a small app to reproduce the issue.

import React from 'react';                                                                                                                                                                             
import { StyleSheet, Text, View, TouchableOpacity } from 'react-native';                                                                                                                               

export default class App extends React.Component {                                                                                                                                                     
  constructor(props) {                                                                                                                                                                                 
    super(props);                                                                                                                                                                                      
    this.state = {                                                                                                                                                                                     
      count: 0,                                                                                                                                                                                        
    };                                                                                                                                                                                                 
  }                                                                                                                                                                                                    

  render() {                                                                                                                                                                                           
    const { count } = this.state;                                                                                                                                                                      
    const extraContent = new Array(200 * count).fill().map((_, index) => (                                                                                                                             
      <View key={index}><Text>Line {index}</Text></View>                                                                                                                                               
    ));                                                                                                                                                                                                

    return (                                                                                                                                                                                           
      <View style={styles.container}>                                                                                                                                                                  
        <View style={styles.actions}>                                                                                                                                                                  
          <TouchableOpacity onPress={() => this.setState({ count: count + 1})}>                                                                                                                        
            <View style={styles.button}>                                                                                                                                                               
              <Text style={styles.buttonText}>Add</Text>                                                                                                                                               
            </View>                                                                                                                                                                                    
          </TouchableOpacity>                                                                                                                                                                          
          <TouchableOpacity onPress={() => count > 0 && this.setState({ count: count - 1})}>                                                                                                           
            <View style={styles.button}>                                                                                                                                                         
              <Text style={styles.buttonText}>Remove</Text>                                                                                                                                            
            </View>                                                                                                                                                                                    
          </TouchableOpacity>                                                                                                                                                                          
        </View>                                                                                                                                                                                        
        <View>                                                                                                                                                                                         
          <Text>Current count: {count}</Text>                                                                                                                                                          
          <View>{extraContent}</View>                                                                                                                                                                  
        </View>                                                                                                                                                                                        
      </View>                                                                                                                                                                                          
    );                                                                                                                                                                                                 
  }                                                                                                                                                                                                    
}                                                                                                                                                                                                      

const styles = StyleSheet.create({                                                                                                                                                                     
  container: {                                                                                                                                                                                         
    flex: 1,                                                                                                                                                                                           
    marginTop: 50,                                                                                                                                                                                     
    width: '100%',                                                                                                                                                                                     
  },                                                                                                                                                                                                   
  actions: {                                                                                                                                                                                           
    flexDirection: 'row',                                                                                                                                                                              
    alignItems: 'center',                                                                                                                                                                              
    justifyContent: 'space-around',                                                                                                                                                                    
  },                                                                                                                                                                                                   
  buttonText: {                                                                                                                                                                                        
    color: '#ffffff',                                                                                                                                                                                  
  },                                                                                                                                                                                                   
  button: {                                                                                                                                                                                            
    alignItems: 'center',                                                                                                                                                                              
    justifyContent: 'center',                                                                                                                                                                          
    backgroundColor: '#95afe5',                                                                                                                                                                        
    height: 50,                                                                                                                                                                                        
    width: 100,                                                                                                                                                                                        
    marginBottom: 5,                                                                                                                                                                                   
    borderRadius: 5,                                                                                                                                                                                   
  },                                                                                                                                                                                                   
});

The code adds and removes some views from the screen when you press and or remove. It's expected that pressing Add three times and then Remove three times one would end with the same amount of memory use. What's really happening is that some of the memory is not released as shown in the graph:

enter image description here

It's interesting that adding again the three times and removing three times the peak of memory consumption is lower than the first round and there's no leak, but if we change to add/remove five times there's an extra pseudo leak.

enter image description here

I call it pseudo leak because from time to time, could understand why, a good portion of this retained memory is released, but it never comes back to the original baseline. It makes me believe that this effect may not be an actual leak, but some kind of cache instead.

In my production app this effect has reached 150+ MB leading to OOM crashes on devices with 1GB of RAM.

Does any one know what is it and if there's a way to avoid this behavior?

Aspirina
  • 676
  • 6
  • 10

2 Answers2

1

Perhaps your onPress props are implicitly returning data unnecessarily. What happens if you put {} after the arrow to prevent a return. Does it make any difference?

You may be able to do something for your setState as well. Consider trying: setState(() => {…code})

This might also be helpful: When to use React setState callback

Shah
  • 2,126
  • 1
  • 16
  • 21
1

Sorry, this is more of a comment, but I wanted to add in some code blocks to help communicate. I think your example is defeating some of React's caching by creating the elements each time inside the render function. Is the result any different if the elements in extraContent are more stable? One way to do this is below:

Create extraContent outside of render, store it in state, and make the remove function return a slice of the array. Something like:

// in constructor
this.state = {
  extraContent: [],
};

// in component
add() {
  const totalElements = this.state.extraContent.length;
  this.setState({ extraContent: this.state.extraContent.concat(new Array(200).fill().map((_, index) => (                                                                                                                             
      <View key={totalElements + index}><Text>Line {totalElements + index}</Text></View>                                                                                                                                               
    ))
  });
}

remove() {
  const totalElements = this.state.extraContent.length;
  if (totalElements > 0) {
    this.setState({ extraContent: this.state.extraContent.slice(0, totalElements - 200) });
  }
}

render() {
  // cut out everything before the return
  // use add and remove functions in your TouchableOpacity components
}

I'm mentioning this because it's possible the problem you've identified in your example is different from the one in your real app, unless you are employing the same strategy for creating components there.

Abe
  • 4,500
  • 2
  • 11
  • 25