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
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%
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.
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.
- 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.
- 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
Install WebP Support forOSX & React Native Project
Host Support
- Install precompiled WebP utilities and libraries from Google: https://developers.google.com/speed/webp/download
- Optional, add WebP Image Preview to OSX: WebPQuickLook
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 projectcurl -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
- Add the following dependency to
android/app/build.gradle
- 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
yarn add TGPSKI/react-native-webp-support
- Open your project in Xcode
- Add
WebP.framework
andWebPDemux.framework
from node_modules/react-native-webp-support/ to your project files (Right click your project and select "Add Files to ...") - Add
WebP.framework
andWebPDemux.framework
to yourLinked Frameworks and Libraries
in the General tab of your main project target - Add “$(SRCROOT)/../node_modules/react-native-webp-support” to your
Framework Search Paths
, located in the Build Settings tab of your main project target - Add
$(SRCROOT)/../node_modules/react-native-webp-support
to yourHeader Search Paths
, located in the Build Settings tab of your main project target - Add
ReactNativeWebp.xcodeproj
from node_modules/react-native-webp-support/ to your project files (Right click your project and select "Add Files to ...") - Add
libReactNatveWebp.a
to yourLink Binary with Libraries
step, located in the Build Phases tab of your main project target - 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/bashSOURCE_DIR=/your/path/here
DEST_DIR=/your/path/here
WEBP_QUALITY=80cd $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/bashOLD_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/bashREACT_NATIVE_SRC_ROOT=/your/path/here
IOS_CP_DEST=/your/path/here
ANDROID_CP_DEST=/your/path/herecd $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 sizeIOS_ASSET_DIR=$IOS_CP_DEST/App/ImagesIOS_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_SIZEecho ANDROID_BUNDLE_SIZE $ANDROID_BUNDLE_SIZE
echo ANDROID_ASSET_SIZE $ANDROID_ASSET_SIZE# Find bundled sizeszip -r ios-cp-archive.zip $IOS_CP_DEST
zip -r android-cp-archive.zip $ANDROID_CP_DESTIOS_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