React Native How Speed Up iOS Build 4x using cache Pods
Tags: reactnative, devops, ci, ios
Published: 2020-11-14 17:17:10

---
title: React Native How Speed Up iOS Build 4x using cache Pods
published: true
description: Optimizing your build times of react native ios apps 
tags: reactnative,devops,ci,ios
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/f0vlwe3atdakrzz7ga21.png 
---


I am that person who hates waiting ⌛ and seems I am lucky that decided to work with iOS apps 🙃 ~~(sarcasm)~~

But anyway I think you should know that pain 😞 when building an iOS app on CI 

---



🖥️ Lets have look our setup [(GitHub Actions)](https://github.com/retyui/react-native-ci-cd-compare/blob/main/ENVINFO.md#github-actions-ios):

```js
  OS: macOS 10.15.7
  CPU: (4) x64 Intel(R) Xeon(R) CPU E5-1650 v2 @ 3.50GHz
  Memory: 8.11 GB / 12.00 GB
```

🗄️ Cache sizes:

```js
node_modules: ~852 MB
pods: ~115 MB
pods derived data: ~1258 MB
```

⏲️ Build time:

![Build time ios app](https://dev-to-uploads.s3.amazonaws.com/i/iopj8ppj8vl9moq7mp3n.png)


So how you can see I save **33m 08s**😲🎉💃!


I hope results impress you and you want to how to get these results? 😉

---


I have read 📖 a lot of articles that describe different approaches:

1) [ccache](https://pspdfkit.com/blog/2015/ccache-for-fun-and-profit/) 
2) [Rome tool](https://engineering.atspotify.com/2019/10/23/when-in-rome-how-spotify-halved-build-times-with-just-one-script/)
3) [cocoapods-binary-cache](https://github.com/grab/cocoapods-binary-cache) - work strangely for me, built only 4 dependencies that no effect on build speed.
4) [cocoapods-binary](https://github.com/leavez/cocoapods-binary) - required to use `use_frameworks!` 


I noticed that that usually changes made on a js side, but native code changes rarely. And a big part of build time takes building native code. 

**So the main idea was to cache Pods build result until `ios/Podfile.lock` changes**


If you use GitHub Actions, your pipeline will be looks like that:

```yml
jobs:
  build_ios:
    runs-on: macos-latest
    steps:

    # Checkout repo, Install deps (node.js, cocacpods, ruby gems) ...

    - uses: actions/cache@master
      with:
        # Path to Derived Data
        path: .local_derived_data
        # Restore cache by Podfile.lock hashsum  
        key: ${{ runner.os }}-pods-derived-data-${{ hashFiles('**/Podfile.lock') }}

    # Run build
```


Also I use a [fastlane](https://fastlane.tools/) tool to run build scripts. So code without optimizations looks like that:


```rb
# Fastfile
platform :ios do
  desc "Build iOS"
  lane :build do
      # Code sign ...

      gym(
        scheme: "MyApp",
        workspace: "./ios/MyApp.xcworkspace",
        export_method: "ad-hoc",
        configuration: "Release",
        clean: true
      )

      # Publish to firebase...
  end
end
```


Nothing special, right? And now version with optimization: 


```rb
platform :ios do
  desc "Build iOS"
  lane :build do
    scheme = "MyApp"
    build_configuration = "Release"
    # !!! Path to the folder that you will cache on CI !!!
    ios_derived_data_path = File.expand_path("../.local_derived_data")
    cache_folder = File.expand_path("#{ios_derived_data_path}/Build/Intermediates.noindex/ArchiveIntermediates/#{scheme}/BuildProductsPath/#{build_configuration}-iphoneos")

    # Code sign ...

    # Step 0) Check if cache exists 

    if(File.exist?(cache_folder))
      # Step 1) Apply a fix of "Copy Pods Resources" Build Phase

      # Before:
      # "${PODS_ROOT}/Target Support Files/Pods-MyApp/Pods-MyApp-resources.sh"
      #
      # After:
      # BUILT_PRODUCTS_DIR=/a/b/c "${PODS_ROOT}/Target Support Files/Pods-MyApp/Pods-MyApp-resources.sh"

      fastlane_require 'xcodeproj'
      project = Xcodeproj::Project.open("../ios/MyApp.xcodeproj")
      target = project.targets.select { |target| target.name == 'MyApp' }.first
      phase = target.shell_script_build_phases.select { |phase| phase.name && phase.name.include?('Copy Pods Resources') }.first
      if (!phase.shell_script.start_with?('BUILT_PRODUCTS_DIR'))
        phase.shell_script = "BUILT_PRODUCTS_DIR=#{cache_folder} #{phase.shell_script}"
        project.save()
      end

      # Step 2) Build only .xcodeproj 
      gym(
        clean: false,
        project: './ios/MyApp.xcodeproj',
        scheme: scheme,
        configuration: build_configuration,
        export_method: "ad-hoc",
        destination: 'generic/platform=iOS',
        export_options: {
          compileBitcode: false,
          uploadBitcode: false,
          uploadSymbols: false 
        },
        xcargs: [
            # Step 3) Provide paths where xcode can't find pods binaries
            "PODS_CONFIGURATION_BUILD_DIR=#{cache_folder}",
            "FRAMEWORK_SEARCH_PATHS='#{cache_folder} $(inherited)'",
            "LIBRARY_SEARCH_PATHS='#{cache_folder} $(inherited)'",
            "SWIFT_INCLUDE_PATHS=#{cache_folder}"
        ].join(" ")
      )
    else

      # Step 4) Build full app .xcworkspace
      gym(
        scheme: "MyApp",
        workspace: "./ios/MyApp.xcworkspace",
        derived_data_path: ios_derived_data_path,
        export_method: "ad-hoc",
        configuration: build_configuration,
        clean: true
      )


      # Step 5) Remove not a Pods binaries to reduce cache size
      require 'fileutils';
      dirs = [
        File.expand_path("#{ios_derived_data_path}/info.plist"),
        File.expand_path("#{ios_derived_data_path}/Logs"),
        File.expand_path("#{ios_derived_data_path}/SourcePackages"),
        File.expand_path("#{ios_derived_data_path}/ModuleCache.noindex"),
        File.expand_path("#{ios_derived_data_path}/Build/Intermediates.noindex/ArchiveIntermediates/MyApp/IntermediateBuildFilesPath/MyApp.build"),
        File.expand_path("#{ios_derived_data_path}/Build/Intermediates.noindex/ArchiveIntermediates/MyApp/IntermediateBuildFilesPath/XCBuildData"),
        File.expand_path("#{ios_derived_data_path}/Build/Intermediates.noindex/ArchiveIntermediates/MyApp/BuildProductsPath/SwiftSupport"),
        File.expand_path("#{ios_derived_data_path}/Build/Intermediates.noindex/ArchiveIntermediates/MyApp/PrecompiledHeaders")
      ]
      dirs.each { |dir| FileUtils.rm_rf(dir) }
    end

    # Publish to firebase...
  end
end
```


---

Some notes to understand what happens:

- First of all, I check the cache exist? (see `Step 0`)

- If it not exists I will invoke `gym()` with default options (see `Step 4`), but add a new options `derived_data_path: ...`. Then a little bit clean this folder, removing a non Pods files. (see `Step 5`)


- If cache successfully restored need to update a Build Phase `[CP] Copy Pods Resources`. (see `Step 1`)


> `[CP] Copy Pods Resources` is run script that CocoaPods automatically adds to your project. It takes care of copying pod resources to the proper directory so that they'll be part of the final archive.

- Then I can invoke `gym()` but it has a different configuration (see `Step 2`). At this time I will build not whole a workspace just project. As I as build the App project, I also need to provide some crucial `xcargs` to help a linker searching Pods at linking time. (see `Step 3`)


```diff
      gym(
-        workspace: "./ios/MyApp.xcworkspace",
+        project: './ios/MyApp.xcodeproj',
+        xcargs: [
+            "PODS_CONFIGURATION_BUILD_DIR=#{cache_folder}",
+            "FRAMEWORK_SEARCH_PATHS='#{cache_folder} $(inherited)'",
+            "LIBRARY_SEARCH_PATHS='#{cache_folder} $(inherited)'",
+            "SWIFT_INCLUDE_PATHS=#{cache_folder}"
+        ].join(" ")
-        clean: true,
+        clean: false,
         # ... 
      )
```

---

Another intriguing strategy to shorten build times:

- https://www.uglydirtylittlestrawberry.co.uk/posts/react-native-ios-build-and-inject-bundle/
- https://reactnative.dev/docs/build-speed
- https://github.com/mikehardy/buildcache-action

---

if you have any question I am glad to discuss them in the comments!

© MurAmur