DEV Community

Cover image for React Native How Speed Up iOS Build 4x using cache Pods
Davyd NRB
Davyd NRB

Posted on • Edited on

React Native How Speed Up iOS Build 4x using cache Pods

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):

  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
Enter fullscreen mode Exit fullscreen mode

🗄️ Cache sizes:

node_modules: ~852 MB
pods: ~115 MB
pods derived data: ~1258 MB
Enter fullscreen mode Exit fullscreen mode

⏲️ Build time:

Build time ios app

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
2) Rome tool
3) cocoapods-binary-cache - work strangely for me, built only 4 dependencies that no effect on build speed.
4) 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:

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
Enter fullscreen mode Exit fullscreen mode

Also I use a fastlane tool to run build scripts. So code without optimizations looks like that:

# 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
Enter fullscreen mode Exit fullscreen mode

Nothing special, right? And now version with optimization:

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
Enter fullscreen mode Exit fullscreen mode

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)
      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,
         # ... 
      )
Enter fullscreen mode Exit fullscreen mode

Another intriguing strategy to shorten build times:


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

© MurAmur

Top comments (7)

Collapse
 
3rdp profile image
3rdp

Thank you for this post. Implementing this right now on GitLab CI. Not using fastlane, so without gym (xcodebuild is not as convenient, but 100% bearable). Got build time from 1h 30m to 38 minutes :yay:

Collapse
 
aoneapp profile image
aoneapp

Can you show us the whole yaml file?

Collapse
 
tgensol profile image
Thibaut

Really cool what you are doing, thanks! Do you do some consulting work ?

Collapse
 
retyui profile image
Davyd NRB

it depends, what exactly you need)

Collapse
 
dirkpostma profile image
Dirk Postma

I created a demo project based on this blog post. See github.com/dirkpostma/react-native...

Also contains example CI script for Azure Pipelines, meant for inspiration

Collapse
 
retyui profile image
Davyd NRB

I saw that you did a presentation) Did you speak in public?

Collapse
 
dirkpostma profile image
Dirk Postma

Not really public, I did it for about 10/15 colleagues, all React Native developers. They really liked it and some of them are going to implement Pods caching in their projects as well. So big thanks to you!