skip to content

In CI for a React Native app, what should you cache for node_modules, CocoaPods and Gradle, and what should each cache key be?

level: middleimportance: should knowfreq 40%

answer

  1. key on the lockfile, not the branch
  2. Podfile.lock keys the Pods cache
  3. pod install still runs: codegen
  4. Gradle caches and wrapper directories
  5. never cache keys or signed outputs

basics

~20 s

Cache dependency downloads keyed on their lockfiles: the JavaScript packages on the package lockfile, ios/Pods on Podfile.lock, Ruby gems on Gemfile.lock, and Gradle's caches and wrapper on the Gradle build files. Still run pod install, and never cache signing material.

solid answer

~40 s

Each cache should be keyed on the file that fully describes its contents, so a change invalidates it and nothing else does. JavaScript dependencies are keyed on the committed lockfile (and the Node version); `ios/Pods` on `ios/Podfile.lock`; Ruby gems for CocoaPods on `Gemfile.lock`; Gradle's `~/.gradle/caches` and `~/.gradle/wrapper` on the Gradle files and `gradle-wrapper.properties`. A Pods cache speeds `bundle exec pod install` up, it does not replace it: in React Native the Podfile's `use_react_native!` runs autolinking and writes codegen output outside `Pods`, so skipping the install leaves the build without generated code. Never cache directories that hold decoded keystores, certificates or signed binaries, and commit every lockfile, otherwise the keys mean nothing.

go deeper

for a junior

Recall the three dependency sets a React Native build downloads and that each cache is keyed on its lockfile.

for a middle

Explain the keys for node packages, gems, Pods and Gradle, and why pod install still runs on a Pods cache hit.

for a senior

Show how you keep caches honest: toolchain versions in keys, scheduled cold builds, hit-rate logging, and keeping signing material and outputs out of every cache.

for a principal

Weigh build speed against reproducibility across many apps sharing runners, and decide which caches are worth the risk of stale state.

## What a React Native build downloads A clean build of a React Native app fetches three dependency sets before it compiles anything: - **JavaScript packages** from the npm registry into `node_modules`, driven by the package lockfile. - **CocoaPods** for iOS into `ios/Pods`, driven by `ios/Podfile.lock`, plus the Ruby gems that run CocoaPods, driven by `Gemfile.lock`. - **Gradle dependencies and the Gradle distribution** for Android into `~/.gradle/caches` and `~/.gradle/wrapper`, driven by the Gradle build scripts, the version catalog and `gradle/wrapper/gradle-wrapper.properties`. Caching them turns minutes of downloads into a restore. The whole question is **what to key each cache on**. ## Keys that invalidate at the right moment A good key is a hash of the file that fully determines the cached content, plus anything outside it that changes the result. | Cache | Directory | Key on | |---|---|---| | JS packages | the package manager's download cache or `node_modules` | lockfile hash + Node version + OS | | Ruby gems | `vendor/bundle` (gitignored in the template) | `Gemfile.lock` hash + Ruby version | | CocoaPods | `ios/Pods` | `ios/Podfile.lock` hash + Xcode version | | Gradle | `~/.gradle/caches`, `~/.gradle/wrapper` | hash of `*.gradle*`, the version catalog, `gradle-wrapper.properties` | Keys **to avoid**: 1. **The branch name**: two commits on one branch with different lockfiles share a stale cache. 2. **A constant key**: the cache is written once and never refreshed. 3. **A key without the OS or toolchain**: a Linux Android job and a macOS iOS job can restore each other's native artifacts. Partial-match fallbacks (restoring the newest cache with the same prefix) are fine for download caches, because the package manager or Gradle verifies and completes them. They are riskier for `node_modules` itself, which should then be reconciled with a clean, lockfile-exact install. ## Why pod install still runs on a cache hit It is tempting to skip `pod install` when `Pods` came from the cache. In a React Native project that breaks the build. The Podfile calls `use_react_native!`, which: - runs **autolinking**, deciding which native modules from `node_modules` become pods; - runs **codegen**, generating the New Architecture native code for the app's specs into `build/generated/ios` under the iOS project, outside `Pods`; - writes a machine-specific `.xcode.env.local` with the runner's Node path if none exists. The cache makes `bundle exec pod install` fast, because the pods are already there; the command itself still has to run. ## What must never be cached - **Decoded signing files**: keystores, `.p12` certificates and provisioning profiles. Decode them outside any cached directory. - **Signed outputs**: `android/app/build/outputs` and Xcode archives are build artifacts to upload, not caches to restore. - **Build directories as a shortcut** to incremental builds: stale intermediate outputs make a release build depend on the previous run instead of the commit. Compiler caches for native code, such as ccache, are a separate speed topic with their own keying rules. ## Lockfiles are the precondition Every key above is a hash of a lockfile, so every lockfile must be committed: the JavaScript lockfile, `Gemfile.lock` and `ios/Podfile.lock`. A project that gitignores `Podfile.lock` resolves pods afresh on every runner, so neither its cache nor its build is reproducible. The template ignores `ios/Pods/` and `vendor/bundle/`, and commits the locks, for exactly this reason. ## Checking the cache is honest - Run a **scheduled cold build** with caches disabled, so a missing dependency hidden by a warm cache surfaces within a day. - Log **hit or miss** per cache and compare build times; a cache that always misses is usually keyed on something that changes every run. - Watch **cache size**: `~/.gradle/caches` grows with every dependency version ever resolved, and an ever-growing restore can cost more time than it saves. Rebuilding it from a fresh key now and then keeps it lean. ## A worked example: the gym-booking app The Android job restores the JavaScript download cache (lockfile key) and the Gradle caches (Gradle-files key), installs, and runs `./gradlew bundleRelease`. The iOS job restores the JavaScript cache, `vendor/bundle` (keyed on `Gemfile.lock`) and `ios/Pods` (keyed on `Podfile.lock` and the Xcode version), then still runs `bundle exec pod install` before `xcodebuild`. Each job decodes its signing files into a temporary directory that no cache step ever lists, and uploads the signed output as a build artifact rather than caching it.

  • The ios/Pods cache hit, so a teammate removed the pod install step from the React Native iOS job; why did the build break?
    In React Native, `pod install` does more than download pods: `use_react_native!` runs autolinking and writes codegen output outside `Pods`. Without it the generated New Architecture code is missing. Keep the step; the cache only makes it fast.
  • Why include the Xcode version in the React Native Pods cache key?
    Pods compiled or configured against one Xcode can misbehave with another, and a runner image update changes Xcode without touching `Podfile.lock`. Adding the Xcode version invalidates the cache exactly when the toolchain moves.

A cache keyed on a lockfile is like a pantry labelled with the recipe's exact ingredient list: change one ingredient and the label no longer matches, so you shop again; keep the same list and you cook from the shelf.

saying these in an interview costs you the question

  • Key every cache on the branch name
  • A Pods cache hit means pod install can be skipped
  • Podfile.lock is generated, so leave it out of git
  • Cache android/app/build to speed up release builds
  • One shared cache for the Linux and macOS jobs is fine