React Native’in ilk yıllarında JavaScript ile native arasındaki iletişim bir posta kutusu gibiydi: JS mesaj yazar, bridge (eski JSON köprüsü) kuyruğa alır, native okuyup cevap verir, cevap tekrar serileştirilip dönerdi. Animasyon frame’leri, kamera işleme, senkron storage gibi yüksek frekanslı işlerde maliyet birikiyordu.
JSI (JavaScript Interface — JS ile native arasında doğrudan çağrı katmanı) bu sorunu köprüyü hızlandırarak değil, gömülü çağrı modeliyle çözer. Native (C++) kod JS motorunun içine yerleştirilir; JS’te normal fonksiyon gibi görünür, arka planda pointer ve host object ile çalışır. New Architecture, Reanimated, MMKV, Vision Camera — hepsi aynı zemine oturur. JSI öğrenmek C++ ezberlemek değil; thread, lifetime ve tip sözleşmesi disiplinidir.
Bridge neden yetmedi?
Klasik bridge’de her çağrı JSON serileştirmesi, kuyruk ve callback eşleştirmesi ister. Async için kabul edilebilir; “her frame 60 kez native oku” senaryosunda maliyet katlanır. Sync okuma pratikte imkânsızdı.
JSI çoğu çağrıda serileştirme maliyetini sıfıra indirir. hostObject.getValue() dediğinizde binding doğrudan C++ metodunu çağırır. Tradeoff: sync güç, sync sorumluluk. Yanlış kullanımda JS thread native iş bitene kadar bloklanır; kullanıcı jank hisseder.
Runtime embedding: motor içinde yaşamak
Hermes (veya alternatif motor) bir jsi::Runtime sunar. Native kod bu runtime’a erken init’te bağlanır ve global’e host object yerleştirir. JS tarafı bunları sıradan API gibi kullanır.
Kavramsal C++ örneği — üretim kodu lib’e göre değişir:
auto fn = jsi::Function::createFromHostFunction(
runtime,
jsi::PropNameID::forAscii(runtime, "add"),
2,
[](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) {
auto a = args[0].asNumber();
auto b = args[1].asNumber();
return jsi::Value(a + b);
});
runtime.global().setProperty(runtime, "nativeAdd", fn);
JS tarafı: global.nativeAdd(1, 2) — arada bridge yok. Init sırası kritiktir: runtime hazır olmadan register etmek release’de sessiz crash veya “undefined is not a function” üretir. Çoğu kütüphane install() pattern’i kullanır; bunu tek sefer ve deterministic tutun.
Neden Hermes + JSI birlikte?
Hermes mobil için tasarlandığından bytecode AOT, düşük bellek ve JSI entegrasyonu birbirini tamamlar. New Architecture ve Hermes varsayılanı birlikte gelince ekosistem tek zemine oturdu. Tradeoff: bazı ES özellikleri gecikmeli gelebilir; JSI modülünüz motor edge case’lerine hazır olmalı.
Host object: C++ sınıfını JS’e açmak
jsi::HostObject ile C++ sınıfınızı JS’te property get/set ve method olarak açarsınız. Turbo Modules’un codegen katmanı bu fikrin ürünleşmiş halidir: Spec yazarsınız, codegen iskelet üretir, siz implementasyonu doldurursunuz.
Reanimated shared value’lar da “JS’ten görünen native bellek” modeliyle çalışır. MMKV aynı aileden: sync storage okuma bridge ekonomisinde imkânsızken JSI ile mümkün olur.
Hot path’te minimal yüzey alanı hedefleyin — gereksiz property getter’ları kaldırın.
Threading: sync ≠ ücretsiz
JSI çağrısı hangi thread’de runtime’a bağlıysa orada çalışır. JS thread’de ağır disk I/O veya karmaşık hesap = frame drop. Reanimated ayrı UI worklet runtime’ı ile çözer; MMKV gibi lib’ler okumanın gerçekten hızlı olduğunu bekler.
Yaygın anti-pattern’ler:
- Sync JSI içinde network (asla)
- Her render’da sync native okuma (cache’leyin)
- Background thread’den JS runtime’a doğrudan erişim (lifetime ihlali)
Doğru pattern: Ağır işi native background’da yapın, sonucu JS thread’e schedule edin. Sync JSI yalnızca milisaniyenin altında operasyonlar için.
Ekosistem haritası
| Kütüphane / katman | JSI kullanımı | Dikkat |
|---|---|---|
| Turbo Modules | Spec → host binding | Codegen + lazy init |
| Reanimated | Shared values, worklets | UI vs JS runtime |
| MMKV | Sync storage | Boyut + migration |
| Vision Camera | Frame processor | Yüksek frekans, GC |
| Kendi C++ lib’iniz | Özel host object | Thread + lifetime |
Kendi modülünüzü yazmadan önce mevcut lib’in JSI binding’ini okuyun.
Güvenlik ve release tuzakları
JS’ten gelen her jsi::Value için tip doğrulaması şarttır. C++ exception doğru köprülenmezse process kill olur — redbox bile görmezsiniz.
Lifetime: Runtime destroy olduktan sonra host object çağrısı use-after-free demektir. Listener register ediyorsanız teardown’da unregister edin.
Release’de R8/ProGuard, JNI ve JSI kayıt sınıflarını silebilir. Keep kurallarını güncel tutun. “Debug’da çalışıyor, release’de crash” JSI’de en sık şikâyettir.
Debug ipuçları
“Undefined is not a function” → install sırası veya library link edilmemiş.
Sadece release’de crash → R8 keep, optimize flag, threading race.
New Arch kapalıyken çalışıyor, açıkken değil → çift yol implementasyonu veya init order farkı.
Hermes heap’te host object sayısı artıyorsa → listener leak.
Production checklist
- Runtime install tek sefer, deterministic
- Tüm JS input’larında tip kontrolü
- Thread politikası dokümante
- Android/iOS parity testleri
- Hermes + New Arch smoke test
- Host object cycle / listener leak audit
- R8/ProGuard keep kuralları CI’da doğrulanıyor
- Sync API’ler için max latency bütçesi (ör. <1ms)
- Teardown path test edildi
Özet
JSI, React Native’i “köprü” olmaktan çıkarıp gömülü JS + native birlikte çalışma modeline taşır. Turbo Modules tip güvenli sözleşme, Reanimated frame bütçesi, MMKV sync erişim sunar — hepsi aynı thread ve lifetime disiplinine dayanır. JSI “sihirli hızlandırma” değil; hangi thread’de ne kadar süre harcadığınızı bilmenin zorunluluğudur.