JSI: direkte C++-Bindings ohne Bridge schreiben
AI generated
RN
native
React Native / New Architecture
JSI
Direkte, synchrone C++-Bindings ohne Bridge schreiben

Das JavaScript Interface, JSI, verbindet JavaScript Runtime und nativen C++ Code direkt, ganz ohne JSON-Serialisierung oder asynchronen Bridge-Round-Trip. Wer ein eigenes HostObject baut, lernt, wie synchrone native Bindings in der Praxis funktionieren, warum der Performance-Gewinn vor allem bei häufigen Aufrufen zählt und wo die Grenzen dieses Ansatzes liegen.

10 Min. Lesezeit JSI HostObject C++ Bindings

1. Warum die klassische Bridge für viele Anwendungsfälle zu langsam war

In der ursprünglichen React Native Architektur liefen alle Aufrufe zwischen JavaScript und nativem Code über die Bridge, einen asynchronen Kanal, der jeden Aufruf inklusive Argumenten in ein JSON-kompatibles Format serialisierte, batchte und über eine Message Queue an den jeweils anderen Thread schickte. Für seltene Aufrufe wie einen einmaligen API-Request war das unproblematisch, aber für Funktionen, die hunderte Male pro Sekunde aufgerufen werden, etwa Animationswerte oder Sensordaten, summierte sich der Overhead aus Serialisierung, Queueing und Thread-Wechsel spürbar auf.

Ein zweites strukturelles Problem war, dass die Bridge grundsätzlich asynchron war. Selbst eine triviale, synchrone Berechnung auf nativer Seite musste als Promise oder Callback modelliert werden, was im JavaScript-Code unnötige Komplexität erzeugte und echte synchrone APIs, wie sie native Plattform-SDKs oft anbieten, praktisch unmöglich machte, ohne auf riskante Workarounds wie NativeModules mit synchronen Methoden zurückzugreifen, die auf Android ohnehin nur eingeschränkt funktionierten.

2. Was JSI wirklich ist: ein C++ Interface zwischen Runtime und nativem Code

Das JavaScript Interface, kurz JSI, ist keine neue Bridge, sondern eine C++ Abstraktionsschicht, die es nativem Code erlaubt, direkt mit der JavaScript Runtime zu sprechen, egal ob diese Hermes, JavaScriptCore oder eine andere JSI-kompatible Engine ist. Statt Daten zu serialisieren, hält JSI Referenzen auf echte JavaScript-Objekte und -Funktionen im Speicher und erlaubt nativem C++ Code, diese Objekte direkt zu lesen, zu verändern oder aufzurufen, so wie es JavaScript selbst auch tun würde.

Dadurch werden synchrone Aufrufe erstmals strukturell möglich: Eine C++ Funktion kann von JavaScript aus aufgerufen werden und sofort einen Wert zurückgeben, ganz ohne Promise, Callback oder Bridge-Round-Trip. Das ist die technische Grundlage, auf der sowohl TurboModules als auch Fabric aufbauen, aber JSI lässt sich auch völlig unabhängig davon nutzen, um eigene, hochperformante native Bindings zu schreiben.

3. HostObject: eigene Objekte direkt in der JS-Runtime bereitstellen

Der zentrale Baustein für eigene JSI-Bindings ist die Klasse jsi::HostObject. Wer sie erbt und die Methoden get und set überschreibt, kann ein C++ Objekt in die JavaScript Runtime einhängen, das sich für JavaScript-Code wie ein ganz normales Objekt verhält, dessen Properties und Methoden aber tatsächlich in C++ ausgeführt werden. Jeder Property-Zugriff aus JavaScript ruft dabei synchron die entsprechende C++ Methode auf.

Das eignet sich besonders für Fälle, in denen native Berechnungen oder native Zustandsabfragen ohne spürbare Latenz aus JavaScript heraus verfügbar sein müssen, etwa ein Kryptographie-Modul, ein Bildverarbeitungs-Kernel oder eine performante Datenstruktur wie ein Ringpuffer für Sensordaten. Anders als bei einem klassischen NativeModule entsteht dabei kein zusätzlicher Bridge-Call pro Zugriff.

4. Praktisches Beispiel: eine synchrone HostObject-Implementierung

Ein einfaches Beispiel ist ein Zähler, der komplett in C++ lebt und aus JavaScript synchron gelesen und erhöht werden kann. Im Konstruktor wird eine get-Methode implementiert, die auf den angefragten Property-Namen reagiert, sowie optional eine set-Methode, falls Schreibzugriffe erlaubt sein sollen. Die Installation erfolgt über eine globale Property auf dem jsi::Runtime-Objekt, meist beim App-Start.

Das folgende Beispiel zeigt eine minimale HostObject-Klasse mit einer lesbaren Property und einer aufrufbaren Methode, jeweils direkt in C++ implementiert, ganz ohne Bridge-Aufruf im Hintergrund.


class CounterHostObject : public jsi::HostObject {
public:
  explicit CounterHostObject(int start) : value_(start) {}

  jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &name) override {
    auto propName = name.utf8(rt);
    if (propName == "value") {
      return jsi::Value(value_);
    }
    if (propName == "increment") {
      return jsi::Function::createFromHostFunction(
          rt, name, 0,
          [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) {
            value_ += 1;
            return jsi::Value(value_);
          });
    }
    return jsi::Value::undefined();
  }

private:
  int value_;
};

// Installation beim App-Start, z.B. in einem eigenen JSI Installer
void installCounter(jsi::Runtime &runtime) {
  auto counter = std::make_shared<CounterHostObject>(0);
  runtime.global().setProperty(
      runtime, "NativeCounter",
      jsi::Object::createFromHostObject(runtime, counter));
}

5. Installation: JSI-Module über TurboModuleManager oder eigenen Installer registrieren

Für produktive Projekte lohnt es sich, ein eigenes JSI-Modul entweder über den ohnehin vorhandenen TurboModule-Mechanismus zu registrieren, wenn es sich in das reguläre Modul-System einfügt, oder über einen eigenen, minimalen Installer, der direkt beim Erzeugen der JavaScript Runtime aufgerufen wird. Letzteres ist üblich für sehr früh benötigte, plattformübergreifende Low-Level-Bindings, etwa Logging oder Performance-Messungen, die noch vor dem Laden des eigentlichen JavaScript-Bundles verfügbar sein müssen.

Wichtig ist, den Installer sowohl in der iOS- als auch in der Android-Runtime-Initialisierung einzuhängen, da beide Plattformen getrennte Einstiegspunkte für die Runtime-Erzeugung haben. Wer das vergisst, bekommt auf einer Plattform einen undefined-Fehler beim ersten Zugriff auf das globale Objekt, was ohne Kenntnis der Installationsreihenfolge schwer zu debuggen ist.

6. Speicherverwaltung: shared_ptr, Runtime-Lebenszyklus und GC-Interop

JSI-Objekte leben in zwei Welten gleichzeitig: als C++ Objekte mit klassischer, referenzgezählter Lebensdauer über std::shared_ptr, und als vom JavaScript-Garbage-Collector verwaltete Werte innerhalb der Runtime. Ein HostObject wird so lange am Leben gehalten, wie mindestens eine JavaScript-Referenz darauf existiert, weshalb zyklische Referenzen zwischen HostObjects und JavaScript-Funktionen zu Leaks führen können, wenn nicht bewusst mit schwachen Referenzen gearbeitet wird.

Ein häufiger Fehler ist, in einem HostFunction-Lambda eine this-Referenz per Kopie statt per shared_ptr zu halten, wodurch das zugrunde liegende C++ Objekt schon zerstört sein kann, während JavaScript noch eine gültige Referenz darauf hält. Solche Bugs äußern sich meist als sporadischer Absturz weit entfernt vom eigentlichen Fehlerort und sind ohne saubere Ownership-Struktur schwer zu reproduzieren.

7. Performance in der Praxis: Bridge gegen JSI bei häufigen Aufrufen

Der Performance-Unterschied zwischen Bridge und JSI zeigt sich am deutlichsten bei Funktionen, die sehr häufig mit kleinen Datenmengen aufgerufen werden. Ein Bridge-Aufruf kostet, unabhängig von der eigentlichen Nutzlast, immer den Overhead aus JSON-Serialisierung, Queueing und mindestens einem Thread-Wechsel, während ein JSI-Aufruf über ein HostObject nur die eigentliche Funktionsausführung kostet, da kein Datenformat konvertiert werden muss.

Bei tausenden Aufrufen pro Sekunde, etwa bei einer Animationsschleife, die pro Frame mehrere native Werte abfragt, macht das den Unterschied zwischen einer flüssigen 60-Hertz-Animation und spürbaren Rucklern aus. Für seltene Aufrufe, etwa einen einmaligen Dateizugriff, ist der Unterschied dagegen kaum messbar, weshalb sich JSI vor allem für Hot-Path-Code lohnt, nicht für jede beliebige native Anbindung.

8. Grenzen von JSI: Thread-Safety und wann Async weiterhin sinnvoll bleibt

JSI-Aufrufe laufen synchron auf dem Thread, von dem aus sie aufgerufen werden, meist dem JavaScript-Thread. Das bedeutet, dass eine lang laufende Operation innerhalb eines HostObjects den kompletten JavaScript-Thread blockiert und damit auch das UI einfrieren lässt, wenn diese Operation vom Hauptthread aus getriggert wird. Für rechenintensive Aufgaben wie große Bildverarbeitung bleibt deshalb ein eigener Hintergrund-Thread mit asynchroner Rückmeldung die richtige Wahl, nicht ein direkter synchroner JSI-Aufruf.

Zusätzlich ist der Zugriff auf ein jsi::Runtime-Objekt selbst nicht threadsicher, da die Runtime für genau einen Thread konzipiert ist. Wer aus einem Hintergrund-Thread heraus Werte an JavaScript zurückgeben will, muss diesen Wechsel explizit über einen dafür vorgesehenen Mechanismus wie runOnJSQueueThread vornehmen, statt die Runtime direkt aus einem beliebigen Thread anzusprechen.

9. Debugging von JSI-Code: native Crashes und Stack Traces

Fehler in JSI-Code äußern sich häufig nicht als übliche JavaScript-Exception, sondern als nativer Absturz mit einem C++ Stack Trace, der ohne Symbolisierung schwer lesbar ist. Für iOS hilft ein an Xcode angehängter Debugger mit aktivierten Debug-Symbolen, für Android liefert der native Crash-Handler in Verbindung mit ndk-stack oder Symbolisierung über den Play Console Crash-Report brauchbare Stack Traces.

Eine typische Fehlerklasse sind ungültige Typumwandlungen, etwa der Versuch, einen JSI-Wert als Zahl zu lesen, obwohl JavaScript tatsächlich undefined übergeben hat. Da JSI bewusst wenig automatische Fehlerbehandlung bietet, um Overhead zu vermeiden, lohnt es sich, an den Grenzen eigener HostObjects konsequent den tatsächlichen Werttyp zu prüfen, bevor er weiterverarbeitet wird, statt auf eine implizite Ausnahme zu vertrauen.

Kriterium Klassische Bridge JSI HostObject Praktische Konsequenz
Aufrufart Ausschließlich asynchron Synchron möglich Sofortige Rückgabewerte ohne Promise
Datenübergabe JSON-Serialisierung Direkte Objektreferenzen Kein Overhead durch Konvertierung
Threading Bridge-Queue auf separatem Thread Läuft auf dem aufrufenden Thread Lange Operationen blockieren bei Fehlgebrauch die UI
Typsicherheit Lose, laufzeitbasiert Explizite jsi::Value Typprüfung nötig Entwickler muss Typen selbst absichern
Einsatzgebiet Seltene, unkritische Aufrufe Hochfrequente Hot-Path-Aufrufe JSI lohnt sich nicht für jede Anbindung

Mironsoft

React-Native-App-Entwicklung und Magento-Anbindung

Eine mobile App zum Magento-Shop, die wirklich rund läuft?

Wir entwickeln React-Native-Apps, die sauber an die Magento REST- oder GraphQL-API angebunden sind, von der ersten Codezeile bis zur Veröffentlichung im App Store und bei Google Play.

App-Konzeption

Architektur und Feature-Umfang einer Magento-angebundenen App gemeinsam planen.

Magento-API-Integration

Produktkatalog, Warenkorb und Checkout sauber an die Shop-API anbinden.

Store-Veröffentlichung

App Store- und Google-Play-Freigabeprozess ohne Stolperfallen begleiten.

10. Zusammenfassung

JSI Direkte Bindings: Das Wichtigste auf einen Blick

JSI im Kern

Ein C++ Interface, das JavaScript Runtime und nativen Code ohne Serialisierung direkt verbindet.

HostObject

Erlaubt eigene C++ Objekte, die sich für JavaScript wie normale Objekte mit synchronen Methoden verhalten.

Performance

Lohnt sich vor allem bei sehr häufigen Aufrufen mit kleiner Nutzlast, kaum messbar bei seltenen Aufrufen.

Grenzen

Synchron blockiert den aufrufenden Thread, lang laufende Arbeit gehört weiterhin in einen Hintergrund-Thread.

11. FAQ: JSI Direkte Bindings: Das Wichtigste auf einen Blick

1Ist JSI eine neue Version der Bridge?
Nein, JSI ersetzt das Konzept der Bridge grundsätzlich durch direkte C++ Referenzen auf die JavaScript Runtime, statt Daten zu serialisieren und über eine Queue zu schicken.
2Kann ich JSI mit Hermes und JavaScriptCore gleichermaßen nutzen?
Ja, JSI ist als Abstraktionsschicht über der jeweiligen Engine konzipiert, solange die Engine das JSI Interface implementiert, funktioniert derselbe C++ Code mit Hermes und JavaScriptCore.
3Was ist der Unterschied zwischen einem NativeModule und einem HostObject?
Ein klassisches NativeModule läuft meist über die Bridge oder TurboModules mit definierten Methodensignaturen, während ein HostObject ein flexibles, direkt in die Runtime eingehängtes Objekt ist, das synchron auf beliebige Property-Zugriffe reagieren kann.
4Blockiert ein synchroner JSI-Aufruf immer die UI?
Nur wenn er vom JavaScript-Thread aus lange läuft, da dieser Thread auch für UI-relevante Updates zuständig ist. Rechenintensive Arbeit sollte weiterhin in einen Hintergrund-Thread ausgelagert werden.
5Wie installiere ich ein eigenes JSI-Modul korrekt?
Über einen Installer, der beim Erzeugen der JavaScript Runtime sowohl auf iOS als auch auf Android aufgerufen wird, meist durch eine globale Property auf dem Runtime-Objekt.
6Warum stürzt meine App bei einem HostObject-Zugriff sporadisch ab?
Häufig wegen fehlerhafter Ownership, etwa wenn ein HostObject zerstört wird, während JavaScript noch eine gültige Referenz darauf hält. Konsequente Nutzung von shared_ptr beugt dem vor.
7Muss ich für jedes native Feature JSI verwenden?
Nein, für seltene oder unkritische Aufrufe reicht ein klassisches TurboModule völlig aus. JSI-HostObjects lohnen sich vor allem für hochfrequente, performancekritische Hot-Path-Aufrufe.
8Kann ich aus einem Hintergrund-Thread direkt auf die JS-Runtime zugreifen?
Nicht direkt, da jsi::Runtime nicht threadsicher ist. Der Zugriff muss über einen dafür vorgesehenen Mechanismus zurück auf den JavaScript-Thread verschoben werden.
9Wie finde ich die Ursache eines nativen JSI-Absturzes?
Über einen an die Runtime angehängten nativen Debugger mit Debug-Symbolen, auf iOS über Xcode, auf Android über ndk-stack oder symbolisierte Crash-Reports.
10Ersetzt JSI TurboModules und Fabric?
Nein, JSI ist die gemeinsame technische Grundlage, auf der sowohl TurboModules als auch Fabric aufbauen, lässt sich aber auch unabhängig davon für eigene, direkte Bindings nutzen.