React Native + WebP: Reducing bundle + binary sizes, increase speed with .webp image format

TLDR: Discussion of bundle / binary size on app installs and React Native performance, convert .png and .jpg images to .webp format, add native decoders to iOS/Android, quantify size savings. Companion repo: https://github.com/TGPSKI/react-native-webp-support
React Native +WebP = Smaller sizes, faster speeds

If your React Native app is anything like the one I most recently worked on, the app complexity, number of image assets, and bundle sizes all increased as the project progressed. If the project is mature, rarely would a developer find a quick fix to remove 5% or more of the total bundle size without a major rewrite.

However, if your app incorporates a large number of .jpg or .png image assets, and you aren’t using .webp image compression, you can achieve significant savings in binary and CodePush bundle sizes by using WebP.

In addition, if you are loading these assets through the JS<->Native bridge, you can achieve a significant performance increase in your application as well.

In my project, I achieved the following improvements with WebP Formatting at 80% quality:

  • Reduced compressed CodePush bundle size by ~69%
  • Reduced compressed iOS and Android binary sizes by ~29%
Size comparisons for PNG vs WebP formatted Images for Sample App

The companion repository can be found here: https://github.com/TGPSKI/react-native-webp-support.


App Size & Install Metrics

Mobile developers should be well familiar with the effects of binary size on app install metrics. Segment put out a great blog post a few years ago, using a published app to prove the relationship between bundle size and app install metrics. They artificially inflated the app size over time, and measured the install metrics for various size thresholds.

Unsurprisingly, as their test app increased in bundle size, the install rate decreased significantly.

The 100–150 MB size threshold is critical.

At this size level, users on slow internet connections or without WiFi access are probably ignoring updates or just outright deleting your application.

Effect of Mobile App Size on Install Metrics — from Segment

If your designer loves using .png images (loading screens, first time experience, high quality company logos), you will want to at least investigate user experience improvements with WebP compression.

If these assets change frequently, or are expected to be included in code push updates, then WebP compression becomes extremely attractive.

Static Asset Loading

There are a two ways to load a static asset in a React Native application.

  1. Native Static Asset: Assets are written to the iOS asset catalog / Android drawables folder, and are bundled with each binary release. The image asset data does not pass through the JS<->Native bridge, and is decompressed and rendered on the native thread. Assets cannot be code pushed with this storage method. Changes to assets must accompany a binary release.
  2. JS Static Asset: Asset paths are set at bundle time and are written to an arbitrary project-relative location. Asset data passes through the JS <-> Native bridge, and is decompressed and rendered on the native thread. Assets can be code pushed with this storage method.

Native assets are always preferred for performance reasons. Anything that offloads data transfer from the JS<->Native bridge is a win for React Native performance, especially if that data is a larger .png of your company logo.

However, native assets are not always practical. CodePush and other solutions that dynamically update bundles are increasingly common and relied on by fast-moving development teams. Assets may need to be changed before the next binary release, and there could be small asset related bugs that need to be fixed right away.

In these cases, how can you optimize the use of JS static images and keep your JS<->Native bridge happy and UI at a high frame rate?

Decrease the size of the JS static images with WebP compression! In apps that use a lot of static images, less data will flow over the JS<->Native bridge, reducing loading and rendering times. In addition, bundle, code push, and binary sizes will be reduced.

This is great and all, but why is WebP not commonly implemented, given the advantages claimed above?

The main reason is that WebP support is not built in to all browsers by default. With React Native, apps are running in a more controlled environment. With WebP libraries that are built into app binaries, we can be sure that users will be able to decode and render our optimized images.

How does WebP Compare to PNG and other formats?

Take a look at a few resources to learn more about WebP compression, and its performance compared to other formats.

WebP Compression Study — Google

PNG vs WebP Image Formats — Andrew Munsell’s Blog

PNG to WebP — Comparing Compression Sizes — Optimus


Install WebP Support forOSX & React Native Project

Host Support

By default, OS X doesn’t provide preview and thumbnail for all file types. This plugin will give you an ability to see previews and thumbnails of WebP images.

# From WebPQuickLook project
curl -L https://raw.github.com/romanbsd/WebPQuickLook/master/WebpQuickLook.tar.gz | tar -xvz
mkdir -p ~/Library/QuickLook/
mv WebpQuickLook.qlgenerator ~/Library/QuickLook/
qlmanage -r

React Native Project — Android

  1. Add the following dependency to android/app/build.gradle
  2. Build a new binary, and use .webp formatted images
...
dependencies {
...
// For WebP support, including animated WebP
compile 'com.facebook.fresco:animated-webp:1.3.0'
compile 'com.facebook.fresco:webpsupport:1.3.0'
  // For WebP support, without animations
compile 'com.facebook.fresco:webpsupport:1.3.0'
}
...

React Native Project —iOS

  1. yarn add TGPSKI/react-native-webp-support
  2. Open your project in Xcode
  3. Add WebP.framework and WebPDemux.framework from node_modules/react-native-webp-support/ to your project files (Right click your project and select "Add Files to ...")
  4. Add WebP.framework and WebPDemux.framework to your Linked Frameworks and Libraries in the General tab of your main project target
  5. Add “$(SRCROOT)/../node_modules/react-native-webp-support” to your Framework Search Paths, located in the Build Settings tab of your main project target
  6. Add $(SRCROOT)/../node_modules/react-native-webp-support to your Header Search Paths, located in the Build Settings tab of your main project target
  7. Add ReactNativeWebp.xcodeproj from node_modules/react-native-webp-support/ to your project files (Right click your project and select "Add Files to ...")
  8. Add libReactNatveWebp.a to your Link Binary with Libraries step, located in the Build Phases tab of your main project target
  9. Build a new binary, and use .webp formatted images

Converting images to WebP Format

Now that you have all the support packages for OSX, iOS, and Android, you can convert your image assets to WebP format. This bash script will bulk convert your image files with cwebp.

#!/bin/bash
SOURCE_DIR=/your/path/here
DEST_DIR=/your/path/here
WEBP_QUALITY=80
cd $SOURCE_DIR
for f in *.png; do
echo "Converting $f to WebP"
ff=${f%????}
echo "no ext ${ff}"
cwebp -q $WEBP_QUALITY "$(pwd)/${f}" -o "${DEST_DIR}/${ff}.webp"
done

File Size Comparison

After converting to WebP format, compare the size differences between your legacy image directory and your new WebP image directory.

#!/bin/bash
OLD_SIZE=$(du -sh $SOURCE_DIR)
NEW_SIZE=$(du -sh $DEST_DIR)
echo $OLD_SIZE $NEW_SIZE

CodePush Bundle Size Comparison

The percentage size reduction between CodePush bundles is directly related to the ratio between your JS bundle file and your image assets.

If your image assets are a large percentage of the total size of your CodePush bundle, you can expect to achieve a significant size reduction.

If static images do not make up a large percentage of your total bundle size, you will not have as big of an improvement compared to other projects.

Here’s a code snippet in shell I used to compare our code push bundle sizes. You may need to make small changes in variables and paths to support your project.

#!/bin/bash
REACT_NATIVE_SRC_ROOT=/your/path/here
IOS_CP_DEST=/your/path/here
ANDROID_CP_DEST=/your/path/here
cd $REACT_NATIVE_SRC_ROOT
# Run react-native bundle command for iOS and Android
## iOS
react-native bundle \
--dev false \
--platform ios \
--entry-file index.ios.js \
--bundle-output $IOS_CP_DEST/index.jsbundle \
--assets-dest $IOS_CP_DEST
## Android
react-native bundle \
--dev false \
--platform android \
--entry-file index.android.js \
--bundle-output $ANDROID_CP_DEST/main.jsbundle \
--assets-dest $ANDROID_CP_DEST
# Find unbundled size
IOS_ASSET_DIR=$IOS_CP_DEST/App/Images
IOS_BUNDLE_SIZE=$(du -sh $IOS_CP_DEST/index.jsbundle | awk '{$NF="";sub(/[ \t]+$/,"")}1')
IOS_ASSET_SIZE=$(du -sh $IOS_ASSET_DIR | awk '{$NF="";sub(/[ \t]+$/,"")}1')
ANDROID_BUNDLE_SIZE=$(du -sh $ANDROID_CP_DEST/main.jsbundle | awk '{$NF="";sub(/[ \t]+$/,"")}1')
ANDROID_ASSET_SIZE=$(du -sh $ANDROID_CP_DEST/drawable-* | awk '{$NF="";sub(/[ \t]+$/,"")}1')
echo IOS_BUNDLE_SIZE $IOS_BUNDLE_SIZE
echo IOS_ASSET_SIZE $IOS_ASSET_SIZE
echo ANDROID_BUNDLE_SIZE $ANDROID_BUNDLE_SIZE
echo ANDROID_ASSET_SIZE $ANDROID_ASSET_SIZE
# Find bundled sizes
zip -r ios-cp-archive.zip $IOS_CP_DEST
zip -r android-cp-archive.zip $ANDROID_CP_DEST
IOS_CP_COMPRESSED_SIZE=$(du -sh ios-cp-archive.zip | awk '{$NF="";sub(/[ \t]+$/,"")}1')
ANDROID_CP_COMPRESSED_SIZE=$(du -sh android-cp-archive.zip | awk '{$NF="";sub(/[ \t]+$/,"")}1')
echo IOS_CP_COMPRESSED_SIZE $IOS_CP_COMPRESSED_SIZE
echo ANDROID_CP_COMPRESSED_SIZE $ANDROID_CP_COMPRESSED_SIZE

Thanks for reading!

Like what you read? Give Tyler Pate a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.