DEV Community

Cover image for React Native How Speed Up iOS Build 4x using cache Pods
David Narbutovich
David Narbutovich

Posted 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

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

Β© MurAmur

Discussion (9)

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
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
David Narbutovich Author

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!

Collapse
aoneapp profile image
aoneapp

Can you show us the whole yaml file?