Caching with Bitrise and React Native – a Post Mortem

Bitrise is currently the build system if you want to automate your mobile builds.

It saves you a lot of time while keeping your build pipelines working compared to an own fastlane build setup. A significant factor here is adapting to frequent changes in Apple’s build and deployment system.

Bitrise makes it very fast to set up build and deployment pipelines. However, it can easily happen that you misconfigure something, as it needs much less in-depth knowledge about Android and iOS deployments.

Our project: the Sono App

We are contributing to the Sono App, a React Native app by Sono Motors. The Sono App allows Sion owners to remote control their solar electric vehicles.

Additionally, the app is the primary customer touchpoint for energy and mobility services, including car sharing, ride pooling, and power sharing.

Typical for a React Native app, we have lots of dependencies that accumulated quickly. We use Firebase for messaging as an example, which adds many dependencies.

Managing all the Node Modules as well as the underlying CocoaPods and Gradle libraries, the built pipelines took longer and longer.

To speed up builds, we used Bitrise caching.
Our pipeline works as follows: We have a starting workflow that runs all javascript tests. If these don’t pass, we don’t need to proceed anyway.

If successful, this workflow triggers two subsequent workflows, one for iOS and one for Android.

Initial caching

We tried to speed up our workflows with caching by using the Bitrise Cache Pull and Bitrise Cache Push steps in each workflow.
The workflows looked like this:

  1. Git Clone
  2. Bitrise Cache Pull
  3. yarn install
  4. on iOS: pod install
  5. Build artifacts
  6. Bitrise Cache Push
  7. Deployment

This workflow, unfortunately, did not work out very well, and our iOS build even sometimes hit the 90 minutes mark – a point where even on the highest Bitrise plan, your build pipeline aborts.

Investigating the problem

We investigated why our pipelines took so long, and we saw that after the main build steps themselves, the cache push steps took the second most time, often over 20 minutes. Investigating this further brought us to these interesting lines in the build log:

Digging into this, we found out that Bitrise has its own caches for each git branch, but it is not possible to have different caches for each workflow.

So what was happening was that every build job, build files first were deleted, then rebuilt, then uploaded to the cache and the whole cycle again next build.

Because the added/removed/changed number of files was high, this took up to 20 minutes.
The numbers changed a bit depending on if the Android or the iOS workflow was uploaded last.

Bitrise per-branch-caches sounds right for a native app that has different branches with long lifespans and different dependencies.
But for us developing a React Native app and creating daily internal releases, it is exactly the opposite of our needs.

Possible solutions

We found two possible solutions for making Bitrises caching mechanism work for us:

  1. Create two separate apps in Bitrise for Android and iOS (and duplicate the test stage)
  2. Unify the Android and iOS workflow

Both did not work that well for us because:

Separate apps for iOS and Android

Separating apps would mean to need maintaining and manually syncing to Bitrise apps with their secrets, workflows, and so on. We didn’t want to increase our management overhead here if there is another way.

Unifying the workflows

Unifying everything to one workflow would work inherently. It would result in our build times being twice as long, though, as we couldn’t build Android and iOS concurrently but instead would build one after another. Also, this would make our workflows less easy to understand.

What we did

First, we removed the Bitrise cache steps from all three workflows. Then, we enabled the built-in caches for all steps that had any (Cocoapods install, yarn install, iOS builds, etc.). Those built-in caches have their own storage, so they don’t conflict with each other.
This modification reduced our build time significantly.

In the end, we added the Bitrise cache only at the workflow, which took the longest (iOS). Unfortunately, it doesn’t have a huge effect because an iOS release requires recompiling every file of every Cocoapods dependency. But now, the caching steps took 6-10 seconds instead of 10-20 minutes.

By now, from the moment we merge a branch into our main branch, it takes ~ 25 minutes to create an internal Android release and ~ 45 minutes for a Testflight release compared to ~45 minutes for an Android release and ~100 minutes for an iOS release.

While there is undoubtedly room for improvement, we will never be super fast while having the dependencies necessary for our architecture (this is just something you need to trade in if you use React Native and Firebase). Ultimately, the pipeline speed is good enough for now™.
Our friends at Sono really like this improvement as the shorter build times imply that their cloud infrastructure footprint decreases (slightly). Win!

If you who are reading this are a Bitrise expert and have a tip here, you are very welcome to drop a hint!

Für neue Blogupdates anmelden:

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.