Guide complet des Nitro Modules en React Native : HybridObjects, générateur Nitrogen, implémentations Swift et Kotlin, comparaison avec les TurboModules et benchmarks de performance pour 2026.
Les Nitro Modules sont un système de modules natifs pour React Native qui permet d'écrire des HybridObjects type-safe en C++, Swift ou Kotlin, avec une génération de code automatique depuis TypeScript. Contrairement aux TurboModules, Nitro élimine le boilerplate Codegen, offre un typage runtime strict et affiche des appels JSI jusqu'à 15× plus rapides. Pour une équipe plateforme qui livre des ponts natifs partagés entre plusieurs apps, c'est la manière la plus productive de coder des modules en 2026, à condition que la New Architecture soit déjà activée.
Honnêtement, quand j'ai migré mon premier module (un wrapper Keychain pour une app fintech) de TurboModules vers Nitro, j'ai divisé le code par trois. C'est ce genre de gain qui m'a convaincu d'en faire mon défaut.
Nitro Modules 0.29 (juillet 2026) exige React Native 0.75+ avec la New Architecture activée : Fabric et Bridgeless obligatoires.
Le générateur nitrogen transforme une interface TypeScript en bindings C++/Swift/Kotlin, sans avoir à écrire de codegenConfig.
Les benchmarks officiels montrent ~15× la vitesse des TurboModules pour un appel synchrone simple et ~59× celle de l'ancien Bridge.
Nitro Views apporte le même modèle aux composants natifs (UIView/ViewGroup) exposés à Fabric.
Le typage est vérifié à la compilation ET au runtime. Les erreurs de type ne passent plus la frontière JS/natif silencieusement.
VisionCamera 4 et react-native-graphics-context sont écrits en Nitro : la bibliothèque est stable en production dans les fintechs et media apps.
Que sont les Nitro Modules ?
Nitro Modules est un framework open source créé par Marc Rousavy (l'auteur de VisionCamera) qui repense la manière d'écrire des modules natifs pour React Native. Là où TurboModules exige un fichier de spec TypeScript, un codegenConfig dans le package.json, un binding C++ généré, puis une classe Objective-C++ ou Java qui hérite de la spec, Nitro remplace toute cette mécanique par un seul outil (nitrogen) et un contrat unique : le HybridObject.
Un HybridObject, c'est un objet JavaScript directement adossé à un objet natif via JSI (JavaScript Interface). Les propriétés et méthodes déclarées en TypeScript deviennent des get/set/call C++ générés automatiquement, avec un pont Swift ou Kotlin quand vous le souhaitez.
Chaque appel traverse la frontière sans sérialisation JSON, avec un contrôle de type au runtime : passer "42" à un paramètre number lève une erreur explicite au lieu d'un comportement indéfini. Pour un architecte plateforme, ce garde-fou est la principale raison d'adopter Nitro dans un monorepo où plusieurs équipes consomment la même API native.
Nitro Modules vs TurboModules : comparaison
Les deux systèmes reposent sur JSI et la New Architecture, mais leurs contrats de développement divergent. Le tableau ci-dessous synthétise les critères que je regarde quand un client hésite entre les deux.
Critère
Nitro Modules 0.29
TurboModules (Codegen)
Contrat source
Interface TypeScript unique
Spec TS + codegenConfig + wrappers natifs
Générateur
nitrogen (Node)
Codegen intégré à React Native
Langages natifs
C++, Swift, Kotlin
Obj-C++, Java (Kotlin possible)
Vitesse d'appel (bench officiel)
~15× TurboModules
Baseline
Type-safety runtime
Oui, stricte
Coercion silencieuse
Composants UI natifs
Nitro Views (Fabric)
Codegen Fabric
Dépendance à la New Architecture
Obligatoire
Obligatoire
Écosystème (juillet 2026)
~180 modules OSS
Universel
Le point qui fait pencher la balance en production, c'est le debugging path. Avec les TurboModules, une erreur de spec se manifeste souvent par un crash JSI opaque au premier appel. Nitro renvoie une exception JavaScript typée (« Nitro: expected number, got string ») que Sentry ou votre wrapper d'observabilité capture proprement.
Si vous cherchez plus de contexte sur la New Architecture elle-même, notre guide de migration Fabric et TurboModules couvre les prérequis JSI, Bridgeless et Hermes en détail.
Prérequis et installation en 2026
Nitro Modules 0.29 requiert React Native 0.75 ou supérieur avec la New Architecture activée : newArchEnabled=true dans android/gradle.properties et RCT_NEW_ARCH_ENABLED=1 dans l'environnement iOS. Hermes est obligatoire côté runtime, car JavaScriptCore ne partage pas les mêmes primitives JSI utilisées par les HybridObjects.
L'installation dans une app existante se fait en trois commandes :
# 1. Ajouter le runtime Nitro à l'app
yarn add react-native-nitro-modules
# 2. Ajouter nitrogen en devDependency à la racine du module
yarn add -D nitrogen
# 3. Pods iOS + prebuild Android
cd ios && pod install && cd ..
Pour un nouveau module natif, la CLI officielle scaffolde l'arborescence complète :
npx create-nitro-module@latest react-native-secure-vault
cd react-native-secure-vault
yarn install
yarn nitrogen
Le générateur crée un dossier nitrogen/generated avec les bindings C++, un projet Xcode et un module Gradle. Ne modifiez jamais les fichiers dans generated/ : ils sont réécrits à chaque exécution de nitrogen. Ajoutez-les à .gitignore et régénérez-les en CI.
Écrire un premier HybridObject
Le contrat d'un module Nitro est une interface TypeScript qui étend HybridObject. Voici un exemple minimal, tiré (avec quelques adaptations) d'un module que j'ai livré cette année : un gestionnaire de secrets qui persiste une clé dans le Keychain iOS ou le Keystore Android.
Le paramètre générique <{ ios: 'swift'; android: 'kotlin' }> indique à nitrogen quels langages natifs implémenteront l'objet. Vous pouvez mixer : { ios: 'swift'; android: 'c++' } est parfaitement valide si vous avez un cœur C++ partagé côté Android.
Côté JavaScript, la consommation est directe :
// src/vault.ts
import { NitroModules } from 'react-native-nitro-modules'
import type { SecureVault } from './specs/SecureVault.nitro'
const Vault = NitroModules.createHybridObject<SecureVault>('SecureVault')
export async function unlockAndRead(key: string) {
const ok = await Vault.requireBiometry('Déverrouiller le coffre')
if (!ok) throw new Error('Biométrie refusée')
return Vault.getItem(key)
}
Aucune enveloppe NativeModules.SecureVault à la Old Architecture, aucun cast, aucun as any. Le type SecureVault est celui que le compilateur TypeScript utilise ET celui que le runtime vérifie. C'est bête à dire, mais après des années de NativeModules mal typés, ça change vraiment la vie.
Nitrogen : la génération de code expliquée
Le générateur nitrogen lit vos fichiers *.nitro.ts, les convertit en AST puis émet trois familles d'artefacts. Un fichier de configuration nitro.json à la racine du module contrôle le tout :
yarn nitrogen
# ✔ Parsed 1 spec file
# ✔ Generated C++ bindings for SecureVault
# ✔ Generated Swift protocol HybridSecureVaultSpec
# ✔ Generated Kotlin abstract class HybridSecureVaultSpec
La sortie contient : un binding JSI C++ (HybridSecureVault.hpp/cpp), un autolinking registrar qui enregistre l'objet sous le nom "SecureVault" auprès du runtime, un protocole Swift et une classe abstraite Kotlin que vous implémentez. C'est tout ce qu'il y a à comprendre. Le pipeline est déterministe et reproductible en CI.
Implémentation en Swift et en Kotlin
L'implémentation Swift ressemble à n'importe quel type conforme à un protocole : pas de @objc, pas de RCT_EXPORT_METHOD, pas d'entêtes Objective-C à exposer.
// ios/HybridSecureVault.swift
import NitroModules
import LocalAuthentication
import Security
final class HybridSecureVault: HybridSecureVaultSpec {
var isBiometryAvailable: Bool {
var error: NSError?
return LAContext().canEvaluatePolicy(
.deviceOwnerAuthenticationWithBiometrics, error: &error
)
}
func setItem(key: String, value: String) throws {
let data = Data(value.utf8)
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
]
SecItemDelete(query as CFDictionary)
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else {
throw NitroError("Keychain write failed: \(status)")
}
}
func getItem(key: String) throws -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
guard status == errSecSuccess, let data = item as? Data else { return nil }
return String(data: data, encoding: .utf8)
}
func requireBiometry(reason: String) throws -> Promise<Bool> {
return Promise.async {
try await LAContext().evaluatePolicy(
.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason
)
}
}
}
Côté Android, l'équivalent Kotlin utilise EncryptedSharedPreferences et BiometricPrompt. Le point clé : les méthodes asynchrones renvoient une Promise<T> Nitro, qui pontifie automatiquement les coroutines Kotlin ou async/await Swift vers le monde des Promesses JS.
// android/src/main/java/com/vault/HybridSecureVault.kt
package com.vault
import androidx.biometric.BiometricPrompt
import com.margelo.nitro.core.Promise
class HybridSecureVault(private val context: Context) : HybridSecureVaultSpec() {
override val isBiometryAvailable: Boolean
get() = BiometricManager.from(context)
.canAuthenticate(BIOMETRIC_STRONG) == BIOMETRIC_SUCCESS
override fun setItem(key: String, value: String) {
encryptedPrefs(context).edit().putString(key, value).apply()
}
override fun getItem(key: String): String? =
encryptedPrefs(context).getString(key, null)
override fun requireBiometry(reason: String): Promise<Boolean> =
Promise.async { promptBiometric(context, reason) }
}
L'enregistrement de l'objet auprès du runtime est généré par nitrogen. Il n'y a rien de plus à câbler dans MainApplication.kt depuis Nitro 0.28.
Nitro Views pour les composants natifs
Nitro Views étend le modèle HybridObject aux composants d'interface. Vous déclarez les props en TypeScript, nitrogen génère un ViewComponent Fabric, et vous fournissez une UIView Swift ou une ViewGroup Kotlin. Aucun ViewManager à écrire, aucune classe SimpleViewManager<T> à hériter.
// src/specs/GradientView.nitro.ts
import type { HybridView } from 'react-native-nitro-modules'
export interface GradientViewProps {
colors: string[]
angle: number
onGradientRendered?: (durationMs: number) => void
}
export type GradientView = HybridView<GradientViewProps, {}, { ios: 'swift'; android: 'kotlin' }>
C'est particulièrement utile pour les composants graphiques haute fréquence (vues caméra, canvas Skia, cartographie), là où l'ancien ViewManager imposait des allers-retours coûteux pour chaque prop. Pour un contexte plus large sur la performance UI, notre article sur l'optimisation du démarrage React Native couvre comment les Nitro Views réduisent le temps d'inflation initial.
Performance : les chiffres réels
Les benchmarks publiés dans le dépôt mrousavy/nitro sur GitHub mesurent des appels vides A→B→A pour comparer les frontières JS/natif. Sur un iPhone 15 Pro en release :
Bridge (Old Architecture) : ~1,17 ms par appel
TurboModules : ~0,027 ms par appel
Nitro HybridObject : ~0,002 ms par appel
Le gain n'est pas anecdotique pour les modules à haute cadence. Un traitement caméra à 60 fps qui délègue au natif via TurboModules dépense ~1,6 ms par frame ; en Nitro, ça tombe sous 0,15 ms. Sur un pipeline de reconnaissance faciale temps réel, c'est la différence entre un rendu fluide et une chute à 45 fps.
Cela dit, gardez la mesure honnête. Si votre module fait un appel réseau ou un accès disque, la surcharge JSI est invisible face à l'I/O. Nitro brille sur les appels très fréquents et très courts. Pour profiler dans votre propre app, notre guide React Native DevTools détaille le panneau Performance et l'échantillonnage Hermes.
Erreurs fréquentes et débogage
Trois pièges reviennent régulièrement dans les revues que je fais :
Oublier yarn nitrogen en CI. Les fichiers generated/ ne doivent pas être commités. Ajoutez une étape run: yarn nitrogen && git diff --exit-code pour bloquer les PR qui divergent de la spec.
Confondre les threads. Un HybridObject peut être appelé depuis le thread JS ou depuis un worklet Reanimated. Les propriétés marquées readonly sont autorisées partout ; les méthodes qui touchent l'UI doivent basculer sur le main thread explicitement.
Passer un null non déclaré. TypeScript accepte string | null mais Nitro exige string | undefined pour l'optionnalité. La vérification runtime lève sinon Nitro: unexpected null. J'ai perdu une demi-journée là-dessus la première fois, alors autant vous éviter le voyage.
Pour observer le trafic JSI, activez NitroModules.debugTrace = true en développement : chaque appel est loggé avec sa durée dans la console Metro. Combinez-le avec le profiler Hermes pour identifier les modules qui saturent le thread JS.
Questions fréquentes
Les Nitro Modules sont-ils prêts pour la production en 2026 ?
Oui. Nitro Modules est stable depuis la 0.20 (mars 2025) et alimente des bibliothèques largement adoptées comme VisionCamera 4 et react-native-graphics-context. Plusieurs fintechs et applications média l'utilisent en production sur des flottes de plusieurs millions d'utilisateurs.
Nitro Modules fonctionnent-ils sans la New Architecture ?
Non. Nitro 0.29 exige Fabric et le mode Bridgeless. Si votre app tourne encore en Old Architecture, vous devrez migrer d'abord, car les HybridObjects reposent sur des primitives JSI qui n'existent pas dans le Bridge asynchrone historique.
Peut-on migrer un TurboModule existant vers Nitro ?
Oui, et progressivement. Vous pouvez conserver votre TurboModule pendant que vous exposez un nouveau HybridObject en parallèle, puis basculer les consommateurs côté JS quand vous êtes prêt. La coexistence est officiellement supportée, ce qui permet une migration sans big bang.
Nitrogen remplace-t-il Codegen ?
Uniquement pour vos modules Nitro. Codegen reste utilisé par React Native lui-même pour les composants Fabric internes et les TurboModules du cœur. Les deux générateurs coexistent sans conflit dans le même projet.
C++ (portable iOS/Android), Swift pour iOS et Kotlin pour Android. Vous choisissez langage par plateforme via le paramètre générique de l'interface, ce qui permet par exemple d'écrire un cœur C++ partagé et une couche Swift fine côté iOS.
Cette erreur signifie qu'un appel JS a passé un type incompatible avec la signature TypeScript. Activez NitroModules.debugTrace, identifiez la méthode fautive dans les logs Metro, puis corrigez le site d'appel. Nitro refuse toute coercion automatique par design.
Guide complet 2026 de React Native DevTools : activation, breakpoints, panneau réseau, profiler React et dépannage. L'outil officiel qui remplace Flipper pour Expo et React Native 0.76+.
Comment migrer un projet React Native existant vers la New Architecture (Fabric, TurboModules, mode Bridgeless) en 2026 : prérequis, activation, audit des dépendances et pièges à éviter.