

React Native ekosisteminde yıllarca uygulamalarımızın performans darboğazı tek bir noktada düğümlendi: Asenkron Bridge. JavaScript ile Native ortam arasındaki her iletişim, verinin JSON formatında serileştirilmesi, bir kuyruğa alınması (batching) ve karşı tarafta tekrar deserileştirilmesi süreçlerinden geçiyordu. Saniyede 60 kare (60 FPS) hedefleyen bir animasyonda veya 120Hz tarama hızına sahip bir ivmeölçer verisini işlerken, bu köprü üzerinde yaşanan 2-3 milisaniyelik ek yük (overhead), doğrudan frame düşüşlerine (jank) ve UI thread bloklanmalarına neden oluyordu.
JSI (JavaScript Interface), bu mimariyi kökünden değiştiriyor. JS motoru (Hermes veya V8) ile C++ belleği arasında doğrudan, senkron bir referans köprüsü kurarak aradaki serileştirme maliyetini sıfıra indiriyor. Geleneksel mimaride 3.000 mikrosaniye süren bir metot çağrısı, JSI üzerinden yapıldığında ortalama 10-15 mikrosaniye bandına iniyor. Bu oran, CPU döngülerinden muazzam bir tasarruf ve gerçek zamanlı veri işleme kapasitesi anlamına gelir.
Bu makalede, React Native uygulamanızda doğrudan bellek erişimi sunan, JavaScript çöp toplayıcısı (Garbage Collector) ile senkronize çalışan bir C++ HostObject modülünün üretim ortamı (production) standartlarında sıfırdan nasıl inşa edileceğini adım adım inceleyeceğiz.
JSI mimarisinin kalbinde facebook::jsi::HostObject sınıfı yatar. Bu sınıf, JavaScript dünyasına maruz bırakacağımız (expose edeceğimiz) C++ nesnelerinin temelini oluşturur. JavaScript motoru, bu nesnenin yaşam döngüsünü std::shared_ptr üzerinden yönetir; yani JS tarafında nesneye olan referans koptuğunda, C++ tarafındaki bellek sızıntısız bir şekilde temizlenir.
İlk olarak, karmaşık matematiksel hesaplamaları senkronize olarak yapacak MathEngine isimli bir sınıf tasarlayalım.
#pragma once
#include <jsi/jsi.h>
#include <vector>
using namespace facebook::jsi;
class MathEngine : public HostObject {
public:
MathEngine();
~MathEngine();
// JS tarafından bir özelliğe (property veya fonksiyon) erişildiğinde tetiklenir
Value get(Runtime& rt, const PropNameID& name) override;
// JS tarafından bir özelliğe değer atandığında tetiklenir
void set(Runtime& rt, const PropNameID& name, const Value& value) override;
// C++ dünyasında belleği doğrudan kullanan sınıfa özel bir metot
std::vector<jsi::PropNameID> getPropertyNames(Runtime& rt) override;
private:
int callCount = 0;
};
Burada get metodu, JS katmanından mathEngine.multiply çağrıldığında devreye giren bir nevi yönlendiricidir (router). JS motoruna hangi değerin veya fonksiyonun döndürüleceğine bu aşamada karar veririz.
Başlık dosyamızı (Header) oluşturduktan sonra, C++ implementasyonuna geçiyoruz. Bu aşamada JavaScript tarafına hem statik bir özellik (property) hem de senkron çalışan bir fonksiyon (method) sunacağız. JS’den gelen argümanların tip kontrollerini atlamamak, production ortamındaki kilit noktalardan biridir.
#include "MathEngine.h"
#include <stdexcept>
MathEngine::MathEngine() {}
MathEngine::~MathEngine() {}
Value MathEngine::get(Runtime& rt, const PropNameID& name) {
auto propName = name.utf8(rt);
// 1. Statik Değer Döndürme
if (propName == "callCount") {
return Value(callCount);
}
// 2. Fonksiyon Döndürme (Senkron İletişim)
if (propName == "multiply") {
return Function::createFromHostFunction(
rt,
PropNameID::forAscii(rt, "multiply"),
2, // Beklenen argüman sayısı
[this](Runtime& runtime, const Value& thisValue, const Value* arguments, size_t count) -> Value {
// Tip kontrolü (Production standartı)
if (count < 2 || !arguments[0].isNumber() || !arguments[1].isNumber()) {
throw JSError(runtime, "'multiply' fonksiyonu iki adet sayısal argüman gerektirir.");
}
double a = arguments[0].asNumber();
double b = arguments[1].asNumber();
this->callCount++; // Durum (state) yönetimi
return Value(a * b);
}
);
}
// Bulunamayan özellikler için undefined dön
return Value::undefined();
}
void MathEngine::set(Runtime& rt, const PropNameID& name, const Value& value) {
auto propName = name.utf8(rt);
if (propName == "callCount") {
throw JSError(rt, "'callCount' özelliği salt okunurdur (read-only).");
}
}
std::vector<PropNameID> MathEngine::getPropertyNames(Runtime& rt) {
std::vector<PropNameID> names;
names.push_back(PropNameID::forAscii(rt, "callCount"));
names.push_back(PropNameID::forAscii(rt, "multiply"));
return names;
}
Yukarıdaki implementasyonda Function::createFromHostFunction ile C++ Lambda fonksiyonunu bir JavaScript nesnesine çeviriyoruz. JSError fırlatmamızın nedeni, standart std::invalid_argument kullanımının doğrudan uygulama çökmesine (SIGABRT) sebep olmasıdır. JSError, hatayı JavaScript katmanında bir TypeError veya Error olarak yakalanabilir hale getirir.
C++ sınıfımız hazır. Şimdi, React Native’in iOS platformunda JS çalışma zamanı (Runtime) belleğine erişip bu sınıfı enjekte etmemiz gerekiyor. Bunun için standart bir React Bridge modülü oluşturup, .mm (Objective-C++) uzantısıyla JSI nesnemizi global objeye bağlayacağız.
// MathInstaller.mm
#import <React/RCTBridge+Private.h>
#import <React/RCTUtils.h>
#import <jsi/jsi.h>
#import "MathEngine.h"
#import "MathInstaller.h"
@implementation MathInstaller
RCT_EXPORT_MODULE()
// JSI Binding işlemleri ana thread'i engellememeli, ancak belleğe yazma işlemi eşzamanlı olmalı.
RCT_EXPORT_METHOD(install)
{
RCTBridge *bridge = [RCTBridge currentBridge];
RCTCxxBridge *cxxBridge = (RCTCxxBridge *)bridge;
if (cxxBridge.runtime == nullptr) {
return;
}
facebook::jsi::Runtime &jsiRuntime = *(facebook::jsi::Runtime *)cxxBridge.runtime;
// C++ Host Object'i std::shared_ptr olarak oluşturuyoruz
auto mathEngine = std::make_shared<MathEngine>();
// JS Tarafında global.mathEngine = MathEngine objesi atamasını yapıyoruz
jsiRuntime.global().setProperty(
jsiRuntime,
"mathEngine",
facebook::jsi::Object::createFromHostObject(jsiRuntime, mathEngine)
);
}
@end
install metodunu JavaScript tarafından uygulamanın başlangıcında bir kez çağırdıktan sonra, mathEngine nesnesi JS Garbage Collector’a teslim edilir ve global bağlamda (window/global) erişilebilir hale gelir.
Android’de JSI entegrasyonu, iOS’a göre daha fazla efor gerektirir çünkü Java ve C++ (JNI) katmanları arasında JS Motoru’nun bellek adresini (pointer) taşımamız gerekir.
Java Modülünde (MathModule.java):
package com.jsiapp;
import androidx.annotation.NonNull;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
public class MathModule extends ReactContextBaseJavaModule {
static {
System.loadLibrary("mathengine");
}
public MathModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@NonNull
@Override
public String getName() {
return "MathModule";
}
@ReactMethod(isBlockingSynchronousMethod = true)
public void install() {
// JS Runtime'ın bellekteki hafıza adresini (pointer) C++ tarafına gönderiyoruz
long jsContextPointer = getReactApplicationContext().getJavaScriptContextHolder().get();
nativeInstall(jsContextPointer);
}
private native void nativeInstall(long jsiPtr);
}
C++ JNI Katmanı (cpp-adapter.cpp):
#include <jni.h>
#include <jsi/jsi.h>
#include "MathEngine.h"
extern "C"
JNIEXPORT void JNICALL
Java_com_jsiapp_MathModule_nativeInstall(JNIEnv *env, jobject thiz, jlong jsiPtr) {
// Pointer'ı jsi::Runtime nesnesine cast (dönüştürme) ediyoruz
facebook::jsi::Runtime* runtime = reinterpret_cast<facebook::jsi::Runtime*>(jsiPtr);
if (runtime) {
auto mathEngine = std::make_shared<MathEngine>();
runtime->global().setProperty(
*runtime,
"mathEngine",
facebook::jsi::Object::createFromHostObject(*runtime, mathEngine)
);
}
}
Bu bağlantı, Java katmanından bağımsız olarak JS ve C++’ın Android üzerinde doğrudan iletişim kurmasını sağlayan kapıyı aralar.
C++ kodunun JavaScript’e tip güvenliği sağlamadan (TypeScript olmadan) aktarılması, production ortamında kritik çalışma zamanı hatalarına yol açar. Global değişkene yazdığımız objenin TypeScript tanımını (Declaration) yapmalıyız.
// MathEngine.ts
import { NativeModules } from 'react-native';
interface MathEngine {
readonly callCount: number;
multiply(a: number, b: number): number;
}
// Global objenin tipini genişletiyoruz
declare global {
var mathEngine: MathEngine | undefined;
}
// Eğer uygulama yeni başlatıldıysa ve bellek henüz yüklenmediyse native modülü tetikle
if (global.mathEngine == null) {
const MathInstaller = NativeModules.MathInstaller ?? NativeModules.MathModule;
if (MathInstaller && MathInstaller.install) {
MathInstaller.install();
} else {
console.error("MathEngine JSI modülü yüklenemedi. Native linkleme hatası olabilir.");
}
}
// Artık global bağlamdan senkron ve güvenli şekilde çağrılabilir
export const getCallCount = () => global.mathEngine?.callCount ?? 0;
export const multiply = (a: number, b: number) => {
if (!global.mathEngine) return 0;
return global.mathEngine.multiply(a, b);
};
| Mimari/Metot | İletişim Türü | Gecikme (Overhead) | Kullanım Senaryosu ve Karar Ağacı (Trade-off) |
|---|---|---|---|
| Eski Bridge | Asenkron (JSON) | 2 – 5 ms | Uzun süren disk işlemleri, arka plan indirmeleri ve UI’ı dondurmaması gereken ağır işlemler. Senkron olma zorunluluğu yoksa en risksiz metottur. |
| Raw JSI (Bu Makale) | Senkron (Doğrudan) | ~15 mikrosaniye | Yüksek frekanslı sensör okuma, SQLite sorguları, real-time ses işleme. Dezavantaj: C++ thread’inde işlem 50ms sürerse, JS thread 50ms kilitlenir (Frame Drop). |
| TurboModules | Lazy Senkron/Asenkron | ~30-50 mikrosaniye | Tip güvenliği sağlayan (Codegen destekli) standartlaşmış yeni mimari. Raw JSI kadar esnek olmasa da, mimari standartları dayattığı için kurumsal projelerde tercih edilmelidir. |
Eğer uygulamanızda çalıştıracağınız C++ fonksiyonu yoğun bir G/Ç işlemi (I/O) yapıyor veya 16.6ms’lik (60 FPS bütçesi) süreyi aşıyorsa, JSI üzerinden fonksiyon çağrısını senkronize bekletmek büyük bir mimari hatadır. Böyle bir senaryoda, C++ tarafında bir worker thread oluşturmalı ve JSI üzerinden JavaScript’e bir Promise döndürerek (jsi::Function ile) asenkron çözümü kendiniz inşa etmelisiniz.
JSI ile C++ kodu yazarken React Native’in varsayılan güvenlik ağının dışında çıkarsınız. Üretim ortamında uygulamanızın çökmelerden (crash) korunması için şu kontrol listesini uygulayın:
jsi::Runtime nesnesi thread-safe değildir. C++ arka plan iş parçacığından (background thread) rt referansına erişmek anında SIGSEGV (Segmentation Fault) hatasına neden olur. Asenkron işlemlerden sonra JS çağırmak için React Native’in CallInvoker yapısını kullanın.HostObject her zaman std::shared_ptr veya std::unique_ptr ile yönetilmelidir. Native işaretçileri (raw pointer) delete ile manuel silmeye çalışmayın; JS çöp toplayıcısına bırakın.std::exception türevleri, JS’e taşınamaz. Tüm kritik kod bloklarını try/catch içine alıp, hataları jsi::JSError olarak yeniden fırlatmalısınız (Adım 2’deki gibi).android/build.gradle dosyanızda ndkVersion tanımının React Native’in talep ettiği (örneğin 0.72 için 23.1.7779620) sürümle birebir eşleştiğinden emin olun.TurboModules, JSI altyapısını kullanan ancak React Native ekibi tarafından standartlaştırılmış, C++ boilerplate kodunu otomatik üreten (Codegen) sistemdir. Raw JSI ise bu makalede yaptığımız gibi HostObject‘i manuel implemente etme sürecidir. Raw JSI, TurboModule mimarisinin dayattığı kısıtlamalara takılmadan özel bellek yönetimi yapmanız gerektiğinde gereklidir.
Hayır. JSI bir soyutlama katmanıdır (abstraction layer). Yazdığınız C++ kodu Hermes, JavaScriptCore (JSC) veya V8 motorlarıyla uyumlu çalışır. Ancak V8 kullanıyorsanız v8-jsi adaptörünü eklemeniz gerekir.
Eğer uygulamanız sadece kullanıcı profili güncellemek, kamera açmak veya bir analitik event’i fırlatmak gibi düşük frekanslı işler yapıyorsa, Bridge yeterlidir ve JSI kodunun karmaşıklığına girmek gereksiz maliyettir. Optimizasyon ihtiyacı sadece veri transferi milisaniyelere sıkıştığında (Video/Audio streaming, lokal veritabanı, sensörler) başlamalıdır.
React Native (0.68+ sürümleri) varsayılan olarak C++17 kullanır. CMake ve Podspec dosyalarınızda standardı -std=c++17 olarak sabitleyerek modern C++ yeteneklerini kullanabilirsiniz. Standart kütüphane (STL) bağımlılıklarını kullanmakta özgürsünüz.
Asenkron Bridge mimarisinden JSI ve C++ Host Object mimarisine geçiş, React Native projelerinizde saf Native (Swift/Kotlin) performansına ulaşmanızı sağlayan en önemli kırılma noktalarından biridir. Seri JSON dönüşümleri ve kuyruklama limitlerini ortadan kaldırarak JS Thread ve Native Thread arasında 15 mikrosaniye tepki süreli, doğrudan senkron bir bağlantı kanalı kurmayı başardık.
Bir sonraki adımınız, uygulamanızda eski Bridge mimarisini en çok yoran veri akışını (örneğin AsyncStorage yerine C++ tabanlı JSI uyumlu MMKV entegrasyonu) analiz edip, bu düğüm noktalarını kendi yazdığınız özel JSI modülleriyle değiştirmek olmalıdır.

Yapay zeka inference altyapılarında karşılaşılan CUDA OOM hatalarının temel nedeni VRAM parçalanmasıdır. ORT 1.17 BFC Arena konfigürasyonu ve trade-off analizi.