Hello there.
So, if you are here you probably saw my previous tweet where I asked for tips & tricks on improving the timing on an iOS/React Native app build time.
What will follow was how I mixed those suggestions + some good old GoogleSearch-fu + me deep diving on this for ~2 days.
For the project I'm working on, this led to:
- ~8 mins improvement on the CI where we are doing E2E using Detox
- ~4 mins improvement on our AppCenter build time for a signed release of the iOS build
First off: this is not a proper blog post. I'm writing it as a gist so that it's clear that it's something quick and dirty. Don't expect it to be perfect.
Second, but actually more important: I'm not fully confident of all these things. What I mean is that quite a few of these changes I've done following my guts over actually being 100% sure that they are in fact correct (I mean yes ofc I've tested the whole thing, and did CI builds and etc, but yeah still not confident). So, please take everything with a massive grain of salt - and don't even think about complaining to me if you follow some of these steps and your build breaks - that's 100% on you.
(actually, if you see anything wrong please let me know!)
I'm using Xcode 11 and React Native 0.61.x at the time of writing.
So, I've started my research on improving the build time of this iOS React Native app in the Xcode world: the reason for it is quite simple - I am pretty sure that just as us in the RN world have a billion different ways and approaches to improving our apps, there must be something similar happening over in the Apple garden.
Turns out, my assumption was true (check the sources at the bottom).
My approach boiled down to opening Xcode, generate builds "with Timing Summary" (top menu -> Product -> Perform action -> Builds with Timing Summary) - this because as every perf improvement you always need to benchmark what you want to improve all along the process - and then changing configurations in the Build Settings tab of our project.
Here are some of the "most relevant" changes that I've made:
(if you search for the flag via the Xcode interface it will find it, in the fancy gui of xcode it may have a different name - also, if you enable the right side panel on Xcode when you select the option it will show you the docs for it SO READ THOSE CAREFULLY)
I'm working on a new app that will only work on iOS 13 by choice, so no reason not to just target this. Inspired by Radek.
Basically, I want to optimize focused on time when I am given a choice. It's likely that it will make the app slightly bigger but I don't mind.
I've enabled this only on debug as I don't need the dSyms. Inspired by this article
GCC_OPTIMIZATION_LEVEL = fast;
GCC_PRECOMPILE_PREFIX_HEADER = YES;
GCC_UNROLL_LOOPS = YES;
Basically, these will make the GCC compiler go faster.
This is an interesting one (btw, in the gui this is the monolithic option). I mean, look at its doc:
Enabling this setting allows optimization across file boundaries during linking. * No: Disabled. Do not use link-time optimization. * Monolithic Link-Time Optimization: This mode performs monolithic link-time optimization of binaries, combining all executable code into a single unit and running aggressive compiler optimizations. * Incremental Link-Time Optimization: This mode performs partitioned link-time optimization of binaries, inlining between compilation units and running aggressive compiler optimizations on each unit in parallel. This enables fast incremental builds and uses less memory than Monolithic LTO.
The only other Xcode side change I've done is not in the build settings, but in the "Edit Scheme..." menu. In the build "left side menu" there, you can find an option to "parallelize build" unselected, and I've activated it.
So, funny story, Pods have a post-install feature. It basically allows you to align the config of each pod to your liking. You just need to add it at the end of your Podfile, like so:
post_install do |installer|
# Improve the Pods project setting to match with the ones we want to have for the project
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0'
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)']
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << 'RCT_ENABLE_INSPECTOR=0'
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << 'ENABLE_PACKAGER_CONNECTION=0'
end
end
end
I've done only a couple of changes to make each Pod be configured more "similarly" to the main project - but it seems that you can really take it to the next level. I mean, look at this!
On CI there are a couple of other things I've done to improve the timing.
So, this may be controversial/not actionable for you. But basically, we had 2 jobs to do the E2E testing (one to setup, the other to run them) - but looking at the timing of caching (saving & restoring) and because of Detox needing to be cleaned & re-build (post restoring cache), we basically were wasting a huge amount of time.
Merging those two jobs back into one removed the need to cache and other quicks we had to do. It was a really EZ win with a massive impact.
It may sound surprising - and I was really surprised to notice a 25 seconds gain from this - but if you have the Metro bundler instance already running before the ios build step, the build is quite faster (if you want to dive deeper into why, check your Xcode project "Build phases" tab, you should see a "Start Packager" step).
You can easily do this on CircleCI via:
- run:
name: Start Metro Packager (Background)
background: true
command: react-native start
(I've done this similarly in the appcenter-pre-build script)
Other random things that could be done to improve the overall speed:
- make the bundling faster by having smaller images, maybe using something like ImageOptim
- add some babel plugins to remove unused code (since we don't have tree shaking like when you use Haul/Webpack) like babel-plugin-transform-remove-console
- etc etc etc (sorry, I know there are a billion different ones but can't think of them right now)
I hope you have found these tips & tricks useful. They have been quite beneficial for me, and I'm basically writing this quick gist to keep memory of them in a way that will help me again in the future.
If you liked this, you may want to follow me over on twitter, early next year I'll basically work on an eBook all about mobile/react native performances and yes, it will be an open source repo. My DMs are open, but in case you are not on twitter but still want to reach out - you actually can now! Write me at notkelset@kelset.dev.
All the above was inspired, in no particular order, by the responses to the tweet linked at the start (thanks to everyone who wrote there!) and quite a bit of links I've found using my magical google-fu:
- https://www.avanderlee.com/optimization/analysing-build-performance-xcode-10/
- CocoaPods/CocoaPods#1347
- https://shashikantjagtap.net/wwdc18-modern-tips-for-optimising-swift-build-time-in-xcode-10/
- https://blog.flexiple.com/xcode-build-optimization-a-definitive-guide/
- https://labs.spotify.com/2013/11/04/shaving-off-time-from-the-ios-edit-build-test-cycle/
- https://patrickbalestra.com/blog/2018/08/27/improving-your-build-time-in-xcode-10.html
- https://blog.flexiple.com/xcode-build-optimization-a-definitive-guide/
- https://pewpewthespells.com/blog/managing_xcode.html
If you liked this experiment / have questions / want to add your feedback to this, please leave a comment below!
@kelset In my experience building the final JS bundle instead of relying on a metro process is even faster and more reliable. If you do that you can also cache the binary itself, swap out the JS bundle and re-sign the app bundle and save a ton of time.