This post is going to look at one approach to optimising a CI/CD pipeline for a React Native app. This could well turn into a series of posts looking at different approaches, most of which can compliment each other.
What is React Native?
Why do we want to do this?
What are our native dependencies?
So we always need a native app package, fine. But how many of the underlying native dependencies change between builds/releases of your app? Let’s have a look at these dependencies might look like:
- The UI frameworks React Native depends on are bundled in the core starter project and the OS itself.
- Most apps will also have other native dependencies - commonly to support navigation, authentication, configuration, monitoring, analytics or to integrate with third party SDKs like Facebook or Google Maps.
- You might have written some of your own Native/Turbo modules.
- Native dependencies also look like new splash screen assets, string assets, permissions or capability configurations, supported OS versions etc.
How many dependencies change between builds?
Some or all of these could change between builds due to platform/third party version upgrades, using new SDKs, design asset changes etc. There are a quite a few factors that could necessitate changes in native dependencies between builds. Lets ask a slightly different question - how often do they change between builds? In an app with a certain level of maturity the answer is usually not that often at all.
Ask yourself what work is most commonly done on your app between builds, this will be relevant later.
About 3-5 minutes for a decent sized app, nice. If no native dependencies have changed can we just rebuild our bundle instead of building the whole app? That is the question I asked and the internet didn’t give me a straight answer so we figured it out for ourselves.
How do we do this?
Below are the steps we need to take to do this successfully, we look at each step in bit more detail
- Check if any native dependencies have changed
- Unpackage the prebuilt app
- Repackage the app
- Resign the app
Have the native dependencies changed?
To decide whether we want to inject a new bundle or build the whole app we need to have a mechanism to detect when the native dependencies we care about have changed. The suggested approach is to hash all of the files that would signal this.
In the repository to go with this post there are some CLI tools to help you with this. In the config folder there is a pretty solid configuration to get you started but make sure you identify all native dependencies your app has and update this. The work that is commonly done on your app between builds will help you here.
generateNativeHash script with the required arguments will produce something along the lines of:
In most CI/CD services there is the capability to save/store artifacts between pipeline executions. Using the hash of this output file to store your built app for iOS could be an idea as you will want it in future executions. Saving this output file itself can be useful to help debug any false positives/negatives dependency changes between executions. A note on this, false positives are fine but false negatives could break your app.
yarn and create-react-native-app) will get this done for us. There are two outputs of this command, the bundle itself and any assets it uses/depends on.
Unpackage the old app
If you have got here you should have a prebuilt
.ipa package for your app, most likely from a previous pipeline execution. Unpackaging this is actually pretty straight forward as it can be treated like a
.zip, with same tooling you would manipulate them with.
Inject the new bundle
Repackage the app
Resign the app
Once you have got to this point you can carry on as normal and test/deploy your app as you are doing already.
Other/complimentary strategies - https://dev.to/retyui/react-native-how-speed-up-ios-build-4x-using-cache-pods-597c
Get in contact
If you have comments, questions or better ways to do anything that I have discussed in this post then please get in contact.