If you ship a React Native build in 2026, the JavaScript that drives your screens, your API calls, and your in-app purchase logic compiles to Hermes bytecode by default. That output looks unreadable at a glance, but public reverse-engineering tools disassemble it within minutes, and the strings inside (URLs, key fragments, business logic) come out plain. A real obfuscation plan stacks several passes: scramble the JavaScript before Hermes, shrink and rename symbols on the native side, and add runtime checks that respond when something tampers with the running build.
Short answer
React Native code is best obfuscated in three layers. Scramble the JavaScript bundle with a transformer such as javascript-obfuscator before Metro hands it to Hermes. Enable Hermes plus R8 on Android and dead code stripping on iOS so symbol names and unused code disappear. Add runtime integrity checks that respond to debuggers, hooks, or repackaged installers. Hermes alone is not enough, because public tools like hermes-dec decompile .hbc files and expose function names, literals, and constants if no JavaScript pass ran first.
What you should know
- Hermes is a JavaScript engine, not an obfuscator. React Native uses Hermes by default, and it compiles your JS to bytecode at release time, which improves startup but does not protect intent.
- Public tools decompile Hermes bytecode. Projects such as hermes-dec and hbctool disassemble .hbc files and produce readable pseudo-JavaScript for recent Hermes versions.
- Obfuscation must run before the Hermes pass. Metro bundles your JS, the transformer rewrites identifiers and string literals, and Hermes then compiles the rewritten output. Reversing this order loses most of the benefit.
- Native shrinking is the other half. R8 renames Java and Kotlin symbols on Android. On iOS, the linker strips dead code, and you can add symbol stripping via build settings.
- OWASP MASVS-RESILIENCE-2 and 3 expect both obfuscation and anti-deobfuscation. Static obfuscation alone satisfies MASVS-RESILIENCE-2. Anti-debug and anti-hook layers are required for MASVS-RESILIENCE-3 in apps that hold user value such as fintech accounts or paid content.
Does Hermes bytecode count as obfuscation?
The short answer is no. Hermes is a JavaScript engine, and the bytecode format it produces is a build artifact rather than a protection. The React Native documentation on Hermes explains that release builds compile JavaScript to .hbc bytecode to improve startup speed and memory footprint. The format is not designed to resist analysis. The bytecode header, opcodes, and string table are well documented, and Hermes itself is open source.
That openness has a direct consequence. The disassembler hermes-dec supports Hermes bytecode versions through the 90s as of 2026, including the versions shipping with current React Native releases. The output is not perfectly recovered JavaScript yet, since loop and conditional structures are not always reconstructed, but it is more than enough to read API endpoints, environment values, and function names that you left in place. A walkthrough on decompiling a Hermes binary by Cognisys Labs takes a reviewer step by step through the same process against a real APK.
In practice, treat Hermes as a build optimization that happens to flatten your JavaScript into a less casual format. It raises the bar slightly, since a quick look at the APK or IPA no longer reveals raw .js files in the assets folder. The bar it raises is measured in minutes, not days. Plan the rest of the pipeline assuming the bytecode will be disassembled.
How do you obfuscate the JavaScript before Hermes compiles it?
The standard pattern in 2026 is to add a Metro transformer that runs javascript-obfuscator, or its React Native wrapper react-native-obfuscating-transformer, on every file before Hermes sees the bundle. The tool renames local identifiers, converts string literals into rotated lookup arrays, splits the array across shuffled accessor functions, inserts dead code paths, and adds self-defending guards that detect injected breakpoints.
Pipeline order matters. Metro reads your source files, the transformer mangles each module, the resulting bundle is concatenated, and Hermes converts that bundle into bytecode at release build time. Running an obfuscator after Hermes does not work, because the bytecode format is fixed and Hermes does not parse JavaScript again.
Useful settings for production include identifier renaming with reserved names for any class referenced through the React Native bridge, string array rotation and shuffle enabled, control flow flattening at a low to medium threshold, dead code injection at around 0.2 to 0.4, and disabled console output. Aggressive settings increase startup time and bundle size; on mid-range Android phones the cost can be 100 to 600 milliseconds of extra cold start, which is noticeable on a first-impression screen. Measure both startup time and memory before locking in a preset.
Two operational notes. First, exclude node_modules from heavy obfuscation, since the dependency code is already public and obfuscating it adds size with little gain. Second, keep a source-map archive in private storage so Sentry, Bugsnag, or Datadog can map traces back to readable lines. Heavily obfuscated stack traces are unreadable without the matching map.
What does R8 on Android and the iOS linker actually do to your bundle?
On Android, the React Native template ships with R8 enabled in release mode. R8 is the successor to ProGuard, and it shrinks, optimizes, and renames Kotlin and Java code. For a React Native build, that means your native modules, your JNI bridge classes, and any third-party SDK code get renamed to short symbols, and unreachable code is removed. The default release template wires this in through minifyEnabled true and shrinkResources true inside the android/app/build.gradle block.
R8 does not touch your JavaScript or your .hbc file. The bytecode lives inside the APK as a raw asset, and R8 has no semantic knowledge of it. That is why JS-level obfuscation is a separate step rather than a flag inside Gradle.
On iOS, the linker strips dead Objective-C and Swift symbols at archive time, and the Strip Style and Deployment Postprocessing build settings inside Xcode control whether debug symbols ship in the IPA. By default, a release archive removes them. Bitcode is no longer accepted by App Store Connect since Xcode 14, so older advice about bitcode obfuscation no longer applies. The remaining iOS native surface (your custom turbo modules, any C or C++ libraries, the Hermes engine itself) is stripped to local symbols at build time.
For both platforms, keep your ProGuard and R8 rules narrow. The most common production failure is a -keep rule pattern that is too broad and leaves entire packages readable in the shipped APK. Tighten the rules so only the classes referenced by the JS bridge and by native modules using reflection are kept by name.
How do you protect API keys and runtime secrets the bundle still has to load?
Obfuscation hides intent. It does not change the fact that any value the running app needs, the app must hold or fetch at runtime. A Stripe publishable key, a Supabase anon key, a Firebase config, an OpenAI proxy URL: each can be retrieved from a compromised device under dynamic analysis. Tools like Frida intercept the values after Hermes has decoded them in memory, regardless of how aggressively the bundle was obfuscated.
The defensive pattern is to keep nothing secret on the device that should not be public. Treat the React Native bundle as a public document. Move any value that authorizes server-side action behind a server you control. Fetch short-lived tokens after the user authenticates, store them in the platform keychain (Keychain Services on iOS, the Android Keystore on Android), and rotate them on the server.
For App Store and Google Play review, this approach also avoids a class of rejections. Apple's review team flags hardcoded production secrets in binaries under Guideline 5.1.1 when the value grants access to user data, and Google flags the same pattern under the Google Play User Data policy. PTKD.com (https://ptkd.com) is one of the platforms that scans the IPA or AAB before submission and lists hardcoded API tokens, AWS keys, and Firebase secrets it can extract, which is useful for catching values that survived the obfuscation pass.
Add a runtime integrity layer if the app holds user funds or paid content. Common controls include jailbreak and root detection, debugger detection through ptrace and TracerPid checks, Frida detection by scanning for known libraries in process memory, and SSL pinning that fails closed on a man-in-the-middle attempt. The first-party Play Integrity API on Android and DeviceCheck and App Attest on iOS cover the device-level half. The OWASP MASVS groups these expectations under the MASVS-RESILIENCE controls, which is the right reference if a security reviewer asks for a written threat model.
| Layer | What it protects | What it does not |
|---|---|---|
| Hermes bytecode | Casual readers, raw .js search | Disassemblers, string extraction |
| javascript-obfuscator | Identifier names, string literals, control flow | Runtime values held in memory |
| R8 on Android | Native Kotlin and Java symbols | The JS bundle and the .hbc file |
| Linker stripping on iOS | Debug symbols, dead native code | Hermes assets inside the IPA |
| Runtime checks | Hooks, debuggers, repackaged installs | A determined attacker on a rooted device |
What to watch out for
Three failure patterns appear often. The first is treating Hermes as the sole defense. A release build still ships the .hbc file, and a reviewer with hermes-dec can produce readable output in minutes. If you tell investors or customers that Hermes obfuscates your code, you are stating something the open-source tooling contradicts.
The second is overdoing identifier renaming on classes the React Native bridge needs by name. The bridge resolves native modules by string, and if the obfuscator renames the class that backs NativeModules.MyModule, the app crashes on first call. Maintain a reserved-names list in the obfuscator config that mirrors every native module exposed through the bridge, plus any class touched by reflection.
The third is forgetting source maps. Once a heavily obfuscated bundle ships, crash reports become unreadable unless your error tracker has the matching source map. Sentry, Bugsnag, and Datadog all accept Hermes source maps; upload them as part of the release pipeline rather than after the fact, since traces from older builds will never resolve against a later upload.
A final myth to reject: there is no React Native build setting that flips on full obfuscation. The pipeline is composed manually from the tools above. Vendors selling a single switch are typically wrapping the same open-source transformers you can wire into Metro yourself.
Key takeaways
- Stack three layers: JavaScript obfuscation, native shrinking, and runtime checks. Any single layer alone is reverse-engineered within hours by readily available tooling.
- Run the JavaScript transformer before Hermes. Hermes compiles whatever Metro hands it, so the obfuscator must sit inside the Metro pipeline, not after the bytecode pass.
- Keep no production secret on the device that should not be public. Move authorizing values behind a server, fetch short-lived tokens, and store them in Keychain Services or the Android Keystore.
- Match the resilience level to the app's value. A free reading app rarely needs anti-debug. Apps holding user funds or paid content should target OWASP MASVS-RESILIENCE-2 and 3.
- Some teams run a build scan against the compiled APK or IPA with platforms like PTKD.com (https://ptkd.com) to catch secrets, encryption libraries, and exposed endpoints that survived the obfuscation pipeline before submission.



