React Native

How we reduced the codepush bundle size by 85%

Introduction

A few weeks ago, I joined a project with 6 developers working on a React-Native application. I quickly faced a problem that I decided to tackle.

The purposes of this article are the following :

  1. Telling you the story of my journey;
  2. Showing you the benefit of a Kaizen by example. This method is a tool that we try to use as much as possible at BAM. My goal here is not to describe this method, but to show its benefit. For more information, you can check this article.

I also assume that you already know the principle of codepush and how it works. If this is not the case, please check the documentation.

Phase 1: Background

The standard process at BAM is once the developer has developed his ticket, he has to deploy it on App Center and then test it on a real device (both iOS & Android). Finally, they can send it to the product owner for validation.

Just a few hours after arriving on my new project, something caught my attention:  they were not testing their features on devices. They had codepush so I did not understand why.

I asked and here are the answers I had :

  • “Codepush does not  work in the app !”
  • “When I click on the codepush download button, I wait & nothing happens!”
  • “I think codepush is broken. Sometimes it works, sometimes not.”
  • “It works every time on iOS. But not on Android. But sometimes it also does not work on iOS. It depends.”

We are working on a medical application purpose. I was very surprised that the testing process was not executed every time & that it was silently ignored by all the team behind the famous “I don’t know. It's not me. It’s the tool.”

This was enough to arouse my curiosity.

Phase 2: Current situation

This is the process that the members of the team were following to download the new codepush version in the app :

I took this process and here is what happened to me :

I've just given up on the idea of having my app updated one day.

At this point the question was: Indeed, why is it not working?

Phase 3 : Set target / goals

Visualize the problem

My first idea was to use the 3rd parameter of the function Codepush.sync(). This parameter allows you to define a function that takes into parameter the download progress. It gives you two pieces of information:

  • receivedBytes: number of bytes received;
  • totalBytes: number of bytes to download;

⇒ More details in the documentation.

I used this information to display a progress bar :

++pre>++code> const [progress, setProgress] = useState(undefined); CodePush.sync( { // ... }, () => { // ... }, (progress) => { setProgress(progress); } ); // In render method ++/code>++/pre>

And this is what I saw (sorry for the bad quality):

I was able now to understand my problem! Codepush was working just perfectly. It was just that the download was taking a lot of time (more than 3 minutes sometimes !).

When I showed this to my team they were really happy. They were able to visualize the update process and wait for the update to finish. After this everybody began testing their features on both iOS & Android. But it was not satisfying enough for me.

I was curious to understand why it was taking so much time.

I quickly knew that the network was not the problem. I listen to music 24h/24h on Spotify at the office.

Something caught my attention. The size of the download: 105648704 bytes. It’s more than 100 Mo. Oh wow.

My goal

This was the metric I decided to follow: the Codepush bundle’s size. My goal was to reduce it as much as possible with low effort.

Phase 4: Root causes analysis

The best way to answer why the bundle was so heavy was to look into it. To do so, you have go on App Center and download your bundle manually. Then add prefix .zip and unzip it;

Inside the folder, the command du -hs ./* gives me these inputs.

++pre>++code> # Android > du -hs ./* 4.0K ./drawable-hdpi 1.4M ./drawable-mdpi 2.7M ./drawable-xhdpi 4.3M ./drawable-xxhdpi 4.0K ./drawable-xxxhdpi 22M ./index.android.bundle 11M ./index.android.bundle.hbc.map 70M ./index.android.bundle.map 88M ./raw ++/code>++/pre>
++pre>++code> #iOS > du -hs ./* 96M ./assets 40M ./main.jsbundle 70M ./main.jsbundle.map ++/pre>++/code>

A Codepush bundle contains 2 things :

  • the JS bundle of your app;
  • all the assets required by your app (images, JSON, etc …).

Hypothesis n°1 :

The first surprise was the presence of source map files :

  • iOS: ./index.android.bundle.hbc.map, ./index.android.bundle.map ⇒ ~81 Mo
  • Android ./main.jsbundle.map ⇒ ~70Mo

When your bundle your application, the bundler combines all your javascript files in a minified one (the .bundle). This file is not “human readable”. This is why you generally generate a source map file that holds the information of the original ones to help you during debugging process. You use it in development when you are debugging, or you upload it on your monitoring tool (like Sentry, Instabug, etc …).

There is no reason to have it in the codepush bundle. We needed to delete it.

Hypothesis n°2 :

Secondly, I took a closer look at two folders :

  • Android: ./raw ⇒ ~88 Mo
  • iOS : ./assets ⇒ ~96 Mo

These folders contain… the assets of your project. More precisely every asset (images, videos, etc…) that you bundle import with require(). Let’s analyze it.

First I searched for the size of all the image files :

++pre>++code> $ find -E packages -regex '.*(png|jpeg)$' | xargs du -sch > 8.3M total ++/pre>++/code>

It’s a lot but it was not explaining the majority of the space. I just ignored it.

Secondly, I searched for the size of the audio files :

++pre>++code> $ find -E packages -regex '.*(mp3)$' | xargs du -sch > 80.6M total ++/pre>++/code>

That time it was different. I finally had a strong explanation. We needed to find a way to reduce it ✅

Phase 5: Countermeasures & implementation

Countermeasure n°1: Deleting source maps

For the deployment part we are using Fastlane :

++pre>++code> sh "yarn appcenter codepush release-react --sourcemap-output --output-dir ./build" # upload sources to sentry for a soft deploy on iOS sh "yarn sentry-cli --auth-token react-native appcenter ./build/CodePush" ++/pre>++/code>

These two lines of code are doing 3 things :

  1. Generate with codepush CLI the bundle and the source map in ./build/Codepush;
  2. Upload with codepush CLI the content of ./build/Codepush;
  3. Upload with Sentry CLI (our error monitoring tool) the content of ./build/Codepush;

The problem is located in step 2. Indeed, it was uploading all the Codepush folder content (i.e. the bundle & the source map). After a small research, I found that it was a known issue: https://github.com/microsoft/appcenter-cli/issues/1451. The App Center team is aware of that but, at the moment, they are focused on maintenance.

The solution

The solution I implemented was to change the source map folder destination to not upload it on App Center. Then put it back on the Codepush folder to upload it on Sentry. The final script looks like this :

++pre>++code> # We need to generate the source map in a different folder with --sourcemap-output-dir sh "yarn appcenter codepush release-react --output-dir ./build --sourcemap-output-dir ./build" # Then we move it back to the CodePush folder to publish it on Sentry sh "mv ../build/main.jsbundle.map ../build/CodePush/main.jsbundle.map" # upload bundle & source map on sentry "yarn sentry-cli --auth-token react-native appcenter ./build/CodePush " ++/pre>++/code>

Result

In less than 0.5 days of work, I reached the following result :

Countermeasure n°2: Migrating assets

Like we said before we had a lot of mp3 files because of all the require() statements we add to our code. Why do we have that many audio files? The response is simple. At the beginning of the project, we were only supporting one language. And months after months, we added new languages. At the same time, this proportion of files slowly grew. And nobody noticed the impact of that.

We had 2 ideas :

  • ❌ The first one was to remove these files from the app and download it on the server when needed. We didn’t choose this one because the bundle issue was not impacting the final user (we do not use codepush in production) and we were worried to consume too much time on a non-anticipated task. It was ruled out & we decided to think about it in a future release;
  • ✅ The second one was to move these files inside the native assets. We chose this option.

The solution

We decided to use https://github.com/unimonkiez/react-native-asset. I’m not going to go deeper, but in the short term, this library allows you to link your assets directly inside the iOS & Android assets. As a result, you can’t anymore load them with require(). Depending on the library you are using you have to use a specific path. Find here an example for the Image component of React-Native: https://reactnative.dev/docs/images#images-from-hybrid-apps-resources.

Result

In 1.5 days of work, I reached the following result :

Phase 6: Follow-up

Here is the report with the impact of all countermeasures implemented :

Conclusion

With these 2 improvements we were able to :

  1. Reduced the size of the codepush bundle by (almost) 85%;
  2. Improve our testing process by reducing the time needed by the app to be updated. Developpers are able to test every tickets quickly. No more excuse.

Following the Kaizen method also helped me in differents ways :

  1. It gave me a framework of thinking;
  2. It gave me a tool to communicate with my stakeholders & teammates. Indeed, I was able to explain to them clearly what I was doing and they were able to see at every step the impact of my actions. As a consequence, challenging me was easier for them;
  3. It was easier for me to write this article 2 months later.

I hope you enjoyed it, learned something  & gave you the curiosity to test this framework on you next challenge !

Développeur Mobile ?

Rejoins nos équipes