Custom Software Consulting & Development

1.347.674.6277
hello@rapidapplabs.com

React Native Experiments: Dynamic Gif Scrolling

by Thomas Lackemann Posted on Dec 20, 2016 in

Making applications for iOS and Android has never been easier thanks to the efforts of the React Native team.

If you know JavaScript, chances are you’ll be able to write an application that runs on any device with minimal effort. Making things look buttery-smooth, however, takes some time and understanding.

In this article we’re going to look at how loading render-heavy content such as gifs can impact the performance of our app and what we can do to significantly boost our performance.

Note: This article assumes intermediate knowledge of React Native. We won’t be discussing code organization or style but rather strict implementation. You should adapt the code to fit your own application.

Introduction

We’re going to be using the Giphy API to fetch lots of results for gifs and render them dynamically. We’ll start off by loading only a few but will populate more results as the user scrolls down the list. We’ll only load more results if the user reaches the bottom of our current results (technically we’ll load them a bit ahead of time to make sure we stay smooth.)

Got it? Let’s start!

ListView

We’re going to start by creating a ListView. This is really the bread-and-butter of creating a buttery-smooth scrolling experience in React Native. It has a few parameters that we’ll tweak to suite our needs so that we keep framerates up and memory consumption down.

In the constructor of our component, we’ll create a new ListView.DataSource that will eventually hold the results from Giphy. We’ll also want to store some stateful variables such as the total amount of results, the current offset, our search query, and how many images we’re fetching at a time.

constructor(props) {
  super(props);

  // Create a ListView.DataSource for the <ListView />
  const imageDs = new ListView.DataSource({
    rowHasChanged: () => false // will explain later
  });

  // Set state
  this.state = {
    apiKey: 'dc6zaTOxFJmzC', // Public Giphy API key (do not use in production!)
    dataSource: imageDs.cloneWithRows([]), // Datasource is immutable, so clone it
    images: [], // We'll use this to append new fetched results
    limit: 20, // How many images to fetch at a time
    totalCount: 0, // Total images available in a query 
    offset: 0, // Current total of fetched images
    term: 'adventure+time', // Search term
  };
}

Now, let’s actually render the ListView, we’ll talk about some of the parameters in a minute.

_renderImage(row) {
  const image = row.images.fixed_width;
  return (
    <Image
      key={`giphy-${row.id}`}
      source={{ uri: image.url }}
      style={{
        margin: 5,
        height: ~~image.height, // ~~ is a hacky (but insanely fast) way to cast to an integer
        width: ~~image.width,
      }}
    />
  );
}

render() {
  const { limit, dataSource } = this.state;
  return (
    <View
      style={{ backgroundColor: '#111', flex: 1 }}
    >
      <ListView 
        contentContainerStyle={{ flexDirection: 'column', flexWrap: 'wrap' }}
        initialListSize={limit}
        enableEmptySections={true}
        dataSource={dataSource}
        renderRow={this._renderImage}
      />
    </View>
  );
}

At this point our app should compile, but nothing really interesting has happened yet. Let’s fix that. Let’s write a function to fetch data from Giphy using some of the state we set earlier.

fetchFromApi() {
  const { apiKey, limit, offset, term, totalCount } = this.state;
  const baseUrl = 'https://api.giphy.com/v1/gifs/search'

  // Only fetch results if we don't have any (initial load) or if we still have images to fetch
  if (totalCount === 0 || offset < totalCount) {
    const url = `${baseUrl}?q=${term}&api_key=${apiKey}&offset=${offset}&limit=${limit}`

    // Fetch images from Giphy
    fetch(url, { method: 'GET' })
      .then(response => response.json())
      .then((response) => {
        const { pagination } = response;
        // Concat the new results with the existing data (remember, immutable datasource)
        const images = this.state.images.concat(response.data);
        // Set the new images, update the offsets
        this.setState({
          images,
          dataSource: this.state.dataSource.cloneWithRows(images),
          totalCount: pagination.total_count,
          offset: pagination.offset + pagination.count,
        });
      })
      .catch((error) => {
        // don't do this, properly handle error
        console.error(error);
      })
  } else {
    // End of results reached don't do anything else
    console.log('End of results');
  }
}

Now we can use this function to initially load some data into our app.

componentWillMount() {
  this.fetchFromApi();
}

Cool! We should have an app that looks something like the below.

Infinite Loading

The ListView component gives us access to a really nice prop called onEndReached which fires once the user has almost reached the bottom of the list. This can be tweaked by also setting the onEndReachedThreshold prop.

Let’s go ahead and load more content when we reach the end of our list, luckily this is one a liner.

...
<ListView 
  contentContainerStyle={{ flexDirection: 'column', flexWrap: 'wrap' }}
  initialListSize={limit}
  enableEmptySections={true}
  dataSource={dataSource}
  renderRow={this._renderImage}
  onEndReached={() => this.fetchFromApi()} // add this line
/>
...

Since we keep track of and act on all the pagination information in the fetchFromApi function, there’s really not much more to this.

We should now have infinitely scrolling gifs, woohoo!

Optimizing

The above code works, it’s pretty fast, but there’s still some things we can do to increase the performance.

Let’s circle back to our constructor.

...
const imageDs = new ListView.DataSource({
  rowHasChanged: () => false // @todo
});
...

Our rowsHasChanged method gives us a way to define what “makes a row unique” so that we don’t spend time re-rendering that particular row every single time we load more content. This is especially useful for when we have hundreds or thousands of rows loaded.

Let’s change that to compare on the id of the row.

...
const imageDs = new ListView.DataSource({
  rowHasChanged: (r1, r2) => r1.id !== r2.id
})
...

Small, but important nonetheless.

There’s still a very big problem with our implementation however. If you took a look at the perf monitor, you would have noticed our RAM increasing into the gigabyte range, not good for something that’s extremely simple. Let’s explore a way to decrease this.

Going Native

One of the major benefits of React Native is the ability to write native components using Java, Swift, or Objective-C. Why would you want to do this? To unleash the raw power of the device, of course! Writing native code allows us to tap into some really, really fascinating SDKs for iOS and Android devices that have solved interesting problems long before React Native was a thing (we’ll take a look at such an SDK shortly.)

As a word of caution, you absolutely do not need to optimize your apps by going native and you should feel free to explore other ways of keeping your app’s resources down. I’m always a believer in “don’t optimize until you have to” but I also like to ship features fast to learn fast over building a highly-scalable solution that maybe ten people will use. 🤷

Understanding the Problem

There’s a really great article about gif optimization on iOS by Flipboard and I really think you should take the time to understand what’s going on under the hood.

To make a long article, short: gifs can be optimized on iOS and Flipboard released an open source library called FLAnimatedImage to make that extremely easy. We’re going to use this native library in place of the React Native <Image /> component to see if we can’t bring down our memory consumption.

With that in mind, let’s optimize our gifs by going native!

Quick Note: I am not an iOS developer so please feel free to reach out if any of the below is wrong in anyway.

Install Module

First, lets install our native framework. We’ll be installing FLAnimatedImage via Carthage as it’s probably the easiest option but it doesn’t really matter how you install it.

For all other frameworks, you’ll obviously need to refer to the specific documention for importing and including it.

# Move to ios folder from project root directory
$ cd ios

Add your framework to your Cartfile (or create a new Cartfile in the ios/ directory.)

# Cartfile
github "Flipboard/FLAnimatedImage"

Install the framework.

$ carthage update

We now need a way to tell iOS about our newly install framework via Carthage. This is fairly easy with Xcode. Open your project’s .xcodeproj file located in the ios/ folder.

Click on your project at the top of the Project Navigator, click “General” and locate the “Embedded Binaries” section. Click the “+” and click “Add Other…”. Locate FLAnimatedImage.framework under ios/Carthage/Build/iOS/ and click “Add”.

Now we’re ready to use this framework.

Writing React Bridge

We need to write a bridge for our app so that our JavaScript can communicate with our new framework.

To keep chugging along, create the following 4 files:

I don’t want to spend too much time going over the details of how the above code works so I encourage you to check out this article to find out more. The above code, simply put, exposes some of the FLAnimatedImage framework functionality for us on the JavaScript side.

Writing React Component

We’re almost done! Now we can create a new component, <Gif /> that we’ll pass a URL so that FLAnimatedImage can take over and do what it does best.

Such a component might look like the following.

const {
  ScaleToFill,
  ScaleAspectFit,
  ScaleAspectFill
} = NativeModules.RNFLAnimatedImageManager;

const MODES = {
  'stretch': ScaleToFill,
  'contain': ScaleAspectFit,
  'cover': ScaleAspectFill
}

class Gif extends Component {
  render() {
    const contentMode = MODES[this.props.resizeMode] || MODES.stretch;
    return (
      <RNFLAnimatedImage 
        contentMode={contentMode}
        {...this.props}
      />
    );
  }
};

Gif.propTypes = {
  contentMode: PropTypes.number,
  src: PropTypes.string,
  resizeMode: PropTypes.string,
  onFrameChange: PropTypes.func,
};

const RNFLAnimatedImage = requireNativeComponent('RNFLAnimatedImage', Gif);

We’ll then replace the <Image /> component in our _renderImage function with the new <Gif /> component.

_renderImage(row) {
  const image = row.images.fixed_width;
  return (
    <View key={`giphy-${row.id}`}>
      <Gif
        src={image.url}
        style={{
          margin: 5,
          height: ~~image.height, // ~~ is a hacky (but insanely fast) way to cast to an integer
          width: ~~image.width,
        }}
      />
    </View>
  );
}

The result?

Over 1,000 gifs loaded and we’re only using a fraction of the resources!

This is far from the best solution, but I hope it shows that with a little understanding of core languages like Objective-C you can write your own React Native bridge to improve performance.

You can grab a full version of this code on GitHub.

Further Reading

If you’re interested in continuing your journey into the scary world of writing native modules, I highly suggest you check out the following.

Conclusion

Writing native code to use in React Native is an excellent way to increase the performance of your application. Off-loading expensive operations to be handled natively instead of over JavaScript enables you to more carefully control your resource consumption, freeing up your JavaScript thread to render heavier user-interfaces with minimal costs.

In the example above we experienced a problem with our memory consumption which caused our app to become resource-hungry and exhaustive on a device. There were many ways to solve this problem, however an easy (and fun) way to solve it was to use a better Gif library for iOS.

If you enjoyed this article (or are looking to hire a good React/React Native developer) I’d love to hear from you! Share this article and be sure to leave a comment below or get in touch with me over at Rapid App.

Happy hacking!

Comments