Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Separating RN Bundle Generation from Android Gradle Build #887

Open
FilDevTronic opened this issue Mar 6, 2025 · 3 comments
Open

Separating RN Bundle Generation from Android Gradle Build #887

FilDevTronic opened this issue Mar 6, 2025 · 3 comments

Comments

@FilDevTronic
Copy link

Introduction

I'm facing a challenge with optimizing our build pipelines and was hoping you might have some insight. Specifically, I'm trying to separate the React Native bundle generation from the main Android Gradle build process so that:

  1. The bundle generation can run as a separate CI job
  2. The resulting bundle, sourcemap, and assets can be cached
  3. These artifacts can then be passed to the Android build jobs

Details

I've tried several approaches:

  • Using react-native bundle directly and moving the generated files to the expected output paths
  • Running just the createBundle Gradle task in a separate CI job
  • Configuring the build.gradle to disable the task when files are present

However, I'm encountering issues with all approaches:

  • Gradle still runs the bundling task instead of marking it UP-TO-DATE
  • R8 fails when the task is disabled, seemingly unable to find main.jar
  • The CI runner-specific metadata seems to prevent Gradle from recognizing pre-existing bundle files

Do you have any recommendations for cleanly separating these concerns in the build process? Is there a better approach I'm overlooking?

I appreciate any guidance you might be able to provide.

Discussion points

  • Splitting the React Native Bundle generation out from the main app building tasks (primarily Android, Gradle; but would also want to do the same for iOS via Xcode)

  • Optimising React Native app
    Building in CI

@cortinico
Copy link
Member

We don't have a single oneliner to do what you're asking for. There are a number of hacky ways to potentially achieve what you want to do.

I would suggest you do the following:

  1. In Job 1, set this in your android/app/build.gradle:
react {
+  debuggableVariants = ["release"]
}

Run the cd android && ./gradlew assembleRelease task. This will build the release APK without bundling as you declared Release as a debuggable variant.

  1. In Job 2, just run cd android && ./gradlew createBundleReleaseJsAndAssets. This will just bundle your app.

  2. Make sure you store the build outputs from Job 1 and Job 2.

  3. In Job 3, which will depend on the completion of Job 1 and 2, run just cd android && ./gradlew assembleRelease. If things are configured correctly, you'll get a lot of UP-TO-DATE tasks and will be good to go.

Also I'm questioning why do you need to do this, as bundling happens on a separate Gradle Task which runs in parallel to the rest of the build so it shouldn't be on the critical path.

I'd suggest you first do a cd android && ./gradlew assembleRelease --scan to understand which task is on your critical path as is worth parallelizing

@FilDevTronic
Copy link
Author

@cortinico, thanks again for your suggestion! I shall try your approach and see if Gradle marks the tasks as UP-TO-DATE this time 🤞🏻.

I hope that the above-mentioned issues with Gradle not recognising that the task already ran, albeit in a separate CI job won't occur again with your suggested steps, though I have my doubts. Running createBundle first, and then running assembleRelease second, locally works flawlessly, with Gradle correctly picking up that the task is UP-TO-DATE, but not in CI.

We are caching the following Gradle related folders - would there be one that's missing that's critical to avoiding this issue?

  • .gradle/caches
  • .gradle/wrapper
  • .gradle/notifications

As to your questioning why we'd want (more than need) to try de-coupling the RN bundle generation from the rest of the build, I'll try to provide salient details that paint a better picture as to what's driving this approach.

Our project is in a transitional phase, consists of a large legacy RN codebase supporting multiple app flavours and environment variants, alongside an increasing amount of native code as we re-write to fully native apps (Kotlin for Android and Swift for iOS). The growth of the native code is outpacing the replacement of the existing RN codebase at this stage, which means the builds in the pipelines are growing consequently bigger.

We’re under pressure to maximize efficiency in our cloud infrastructure usage. That means we need to reduce our CI job run times and keep the resource footprint of our pipeline runners as lean as possible.

For additional context:

Overall project
A bare-flow React Native 0.73.11 app, leveraging Expo 50 solely for Expo modules—without using Expo’s full build system or other services.

CI/CD Pipelines
We're running on GitlabCI, with our CI runners used for the Android Gradle builds having:

  • 16GB memory
  • 8 CPU cores
  • linuxamd64 arch

Gradle & JVM Configuration

org.gradle.jvmargs = -Xms512m -Xmx5596m -XX:MaxMetaspaceSize=1024m -Dfile.encoding=UTF-8 -XX:+UseG1GC -XX:+UseStringDeduplication -XX:+OptimizeStringConcat -Xlog:gc*=debug:stdout:time,uptime,level,tags -XX:MaxGCPauseMillis=2000 -XX:ParallelGCThreads=3 -XX:ConcGCThreads=3 -XX:G1HeapRegionSize=8m -XX:InitiatingHeapOccupancyPercent=35 -XX:+UnlockExperimentalVMOptions -XX:G1MixedGCLiveThresholdPercent=35 -XX:G1HeapWastePercent=5 -XX:G1OldCSetRegionThresholdPercent=10 -XX:G1ReservePercent=10 -XX:SurvivorRatio=4 -XX:G1MaxNewSizePercent=50 -XX:MinMetaspaceFreeRatio=20 -XX:MaxMetaspaceFreeRatio=40 -XX:G1MixedGCCountTarget=16
org.gradle.parallel = true
org.gradle.workers.max = 2
org.gradle.caching = true
org.gradle.cache.expiration.days=3
kotlin.daemon.jvmargs = -Xms512m -Xmx1536m -XX:MaxMetaspaceSize=512m
kotlin.compiler.execution.strategy = daemon
android.enableJetifier = true
android.useAndroidX = true
android.defaults.buildfeatures.buildconfig=true
reactNativeArchitectures = arm64-v8a
newArchEnabled = false
hermesEnabled = true

As seen above, we are using the Gradle Build Cache, and parallel tasks.

However, we currently aren't able to use the Gradle Configuration Cache (at least for the React Native side of things), as it's not working in React native 0.73. Thank you for your very recent work, re-enabling it in main. We're currently working on upgrading to RN 0.76/Expo 52, and once done, I can patch in the changes you've made to the react-native-gradle-plugin to re-enable it. 🙏🏻

CI Runners and Build Tasks
We are currently constrained to 16GB of memory for our CI runners, and trying our hardest to not reach out for the simple solution of just increasing the runner size.

Whilst the project was entirely and only a React Native/Expo codebase, 16GB was fine, and jobs took around ~35-45 minutes to build the app per-flavour, per-env, with a build cache.

Now, with a growing native codebase on top of the existing RN code, we are often seeing the build CI jobs getting OOM killed. We noted that this often happens right around when the createBundle RN bundling task is running, or if not OOM, the task will often keep running until our globally set CI job timeout of 1 hour.

Additionally, Gradle 8’s more aggressive R8 optimizations have added to the resource load during builds.

By decoupling the RN bundling into its own CI job, we aim to cache the output (bundle, sourcemap, and assets) separately, reducing redundant work and ultimately keeping our CI job times and resource usage in check.

@cortinico
Copy link
Member

We are caching the following Gradle related folders - would there be one that's missing that's critical to avoiding this issue?

You would have to cache all the /build folders in the various Gradle project (you might have a LOT of them depending on how many dependencies you have).

That's one of the reason why optimizing this to be a single Gradle invocation and make sure it runs as fast/parallel as possible is the way to go.

However, we currently aren't able to use the Gradle Configuration Cache (at least for the React Native side of things), as it's not working in React native 0.73

Configuration Caching is coming in React Native 0.79

Have you also consider using a distributed build cache like https://gradle.com/develocity/ ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants