React Native uygulamalarında, yazılan JavaScript kodunun cihazda çalıştırılabilmesi için varsayılan olarak JavaScriptCore (JSC) motoru kullanılır. JSC motoru iOS cihazlarda, Safari tarayıcısı ile birlikte tümleşik halde gelirken, Android cihazlarda ise harici olarak APK’ya eklenmektedir. Bu nedenle JSC, varsayılan olarak iyi bir JavaScript motoru olmasına karşın, büyüklüğünden dolayı Android’de uygulamanın boyutunu arttırmaktadır. Ayrıca cihazda çalışan JavaScript kodunun parse edilmesi ve bytecode’a çevrilmesi uygulamanın yavaş bir şekilde açılmasına neden olmaktadır.
Örnek bir JavaScript kodunun Hermes tarafından dönüştürülmüş dump-bytecode hali (solda) derlenmiş emit-binary hali (sağda).
Bu problemden dolayı, daha düşük işlem gücüne sahip mobil cihazlarda çalışacak şekilde JavaScript performansının iyileşmesi gereklidir. Buradan yola çıkarak Facebook ekibi Hermes adında yeni bir JavaScript motoru yaptığını duyurdu. Hermes motoru, Google Chrome tarayıcısına hayat veren V8 engine gibi genel amaçlı olarak kullanılan diğer JavaScript motorlarının aksine, React Native uygulamaları için özel olarak üretilmiştir. Bu amaçla, daha az RAM’i ve yavaş bir depolama alanına sahip düşük konfigürasyonlu cihazlarda dahi çalışabilmek için tasarlanmıştır.
Hermes engine nedir?
Hermes Engine, React Native uygulamasının build edilmesi esnasında, JavaScript kodunun parse edilip derlenerek bytecode haline getirildikten sonra, mobil cihazda hızlı bir şekilde çalıştırılmasını sağlayan bir JavaScript motorudur. Kodun parse edilmesi ve bytecode oluşturulması build esnasına taşındığı için, mobil cihazda bu işlemlerin yapılmasına gerek kalmaz ve uygulama daha hızlı bir şekilde açılır. Ayrıca daha yüksek bir performans sergiler.
React Native uygulamalarının performansına nasıl etki ediyor?
Birçok mobil uygulamada olduğu gibi JavaScript çalıştıran React Native uygulamalarında da kullanıcı deneyimini iyileştirmek için aşağıdaki metriklere dikkat etmek gerekiyor:
- Uygulamanın açılma hızı (Time to interactive).
- İndirme boyutu (APK boyutu).
- Bellek (RAM) yönetimi. Bu metrikler üzerinde daha iyi işlem yapabilmek için halihazırdaki diğer JavaScript motoru çözümleri gereksinimleri karşılamıyordu. Bu nedenle Facebook ekibi, sıfırdan başlayarak Hermes’i üretti. Bu sayede React Native uygulamalarının performans artışı konusunda önemli bir atılım yapıldı.
Hermes motorunda alınan mimarî kararlar
Mobil cihazlarda RAM’in az olması ve bellek erişiminin yavaşlığından dolayı Facebook ekibi belirli mimari kararları aldılar. Uygulamaların daha performanslı hale getirilmesi için Hermes motorunda aşağıdaki özelliklerle geliştirdiler.
Bytecode’a derleme işlemi (precompilation)
Normalde bir JavaScript motoru, ilgili JavaScript kodunu çalıştırmak için öncelikle kodu yükler, sonra parse eder (ayrıştırır) ve çalıştırmak üzere ilgili bytecode’u üretir. Bu sürecin mobil ortamda yürütülmesi, JavaScript kodunun çalıştırılması için gecikmeye sebep olmaktadır. Bu nedenle Hermes, uygulamanın henüz paketlenme aşamasında iken, bytecode’unun hazırlanarak APK’ya dahil edilmesi sayesinde, uygulamanın daha hızlı açılmasını sağlamaktadır. Ayrıca oluşan bytecode’un, paketlenmeden önce optimize edilebilmesi de mümkün hale gelmektedir.
Hermes kullanılarak, bytecode’un özel bir şekilde tasarlanması sayesinde, bytecode dosyasının tüm içeriğinin tamamen okunması gerekmeksizin, belleğe map edilmesi ve yorumlanması mümkün hale geldi. Dolayısıyla düşük ve orta seviyeli mobil cihazlardaki yavaş depolama alanından dolayı kaynaklanan problemler de, bytecode’un sadece gerektiğinde yüklenmesi sayesinde giderilmiş oldu.
Ayrıca Android işletim sistemine sahip cihazlarda depolama alanı olarak flash bellek kullanılmaktadır. Flash bellek, halihazırda bilgisayarlarda kullandığımız SSD’ler gibi değildir, ve bu belleğin belirli bir yazma ömrü vardır. Dolayısıyla Android’de belleğin swap edilmesi gibi bir kavram yoktur. Bu nedenle Android’de bellek, bir dosya tarafından desteklenmekte ve salt okunur olarak eşlenme sağlanmaktadır (paging). Bu nedenle Android’de, aşırı bellek tüketiminde arka plandaki uygulamalar kill edilebilmektedir. Hermes sayesinde, düşük RAM’e sahip cihazlarda bellek tüketiminden dolayı kaynaklanan uygulamaların kill edilmesi işlemlerinin azalması amaçlanmaktadır.
Swap işlemi: RAM’deki process’lerin diske kaydedilmesi gösterilmiştir. Android’de ana bellek “flash memory” olduğundan ve yavaş olduğundan dolayı, swap edilmemektedir.
Hermes’te JIT bulunmuyor
Birçok JavaScript motorunda olduğu gibi, hızlı çalışma açısından kod içerisinde sıklıkla kullanılan interpreted kodlar makine koduna derlenmektedir. Bu olay JIT (Just-in-Time) derleycisi sayesinde yapılmaktadır.
Google Chrome’daki JIT derleyicisinin çalışma evreleri – Kaynak: https://hackernoon.com/webassembly-the-journey-jit-compilers-dfa4081a6ffb
Hermes’te JIT bulunmadığı için CPU temelli bazı benchmark testlerinde rakiplerine göre daha az puan alabilir. Fakat zaten halihazırdaki benchmark testleri, mobil uygulamanın iş yükünü genellikle doğru bir şekilde temsil edememektedir. Bununla birlikte Hermes için bu doğrultuda JIT ile ilgili deneysel çalışmalar da yapılmasına rağmen, daha önce anlattığımız 3 metriği bozmayacak şekilde JIT geliştirimi yapmak oldukça zor görünmektedir. Çünkü, uygulama çalıştığı anda JIT de ayağa kalkmak zorunda olduğundan dolayı, uygulamanın açılma süresine yarardan çok zararı dokunabilmektedir. Ayrıca JIT, oluşan native kodun büyüklüğünü ve bellek kullanımını da arttırmaktadır. Bu nedenle Hermes içerisinde JIT eklenmemiştir ve bunun yerine interpreter (bytecode yorumlayıcı) performansı üzerine yoğunlaşılmıştır.
Garbage collector stratejisi
Mobil cihazlarda bellek kullanımı oldukça önemlidir. Nispeten ucuz ve düşük performanslı cihazlarda RAM miktarı oldukça sınırlıdır. Genellikle swap işlemi yoktur ve işletim sistemi, yüksek bellek tüketimine sahip uygulamaları agresif bir şekilde kill etmektedir. Uygulama kill edildiğinde, yeniden başlatılması yavaş bir şekilde gerçekleşmekte ve bunun sonucunda arka plana atılan uygulamalardan geri dönme deneyimi daha kötü bir hale gelmektedir.
Hermes motoru yapılma aşamasında iken, 32 bit cihazlar üzerinde gerçekleştirilen testlerde, sanal adres uzayının (virtual address space) sınırlı bir alan olduğu farkedilmiştir. Bu nedenle, Hermes tarafından kullanılan bellek tüketimini ve sanal adres uzayını azaltmak için aşağıdaki özellikte bir garbage collector oluşturulmuştur:
- On-demand allocation (Sadece istenildiğinde bellek tahsis etme): Sadece ihtiyaç duyulduğu anda sanal adres uzayının parçalar halinde tahsis edilmesi gerçekleştirilmiştir.
- Noncontiguous (Birden fazla sanal adres uzayları): 32 bitlik cihazlardaki kaynak kısıtlaması problemini çözmek için birden fazla sanal adres uzayı kullanılmaktadır.
- Moving (Bellek adreslerinin yer değiştirilmesi): Objelerin bellek üzerinde yerlerinin değiştirilmesi sayesinde, bellek defragmantasyonu yapılabilir. Böylece bellekteki ayrı parçaların işletim sistemine geri gönderilmesine gerek kalmaz.
- Generational (Aşamalı okuma işlemi): Her garbage collection işleminde tüm JavaScript heap’inin okunmaması sayesinde, GC duraklatma süreleri (GC pause time) düşmektedir.
Geliştirici deneyimi
Hermes nasıl etkinleştirilir?
Android uygulamalarında Hermes motorunun aktifleştirilmesi oldukça basittir. Tek yapılması gereken build.gradle
dosyasında enableHermes:true
yeterli olmaktadır.
Geliştirim yapılırken cihazda derleme işlemi
JavaScript bazlı bir platformda, uygulamanın tekrar başlatılmasının hızlı gerçekleştirilmesi geliştiriciler için de avantajlı bir durum oluşturmaktadır. Fakat geliştirim yapılırken, bytecode’un derlenme esnasında oluşturulması ve mobil cihaza gönderilmesi bu hızı yavaşlatmaktadır. Bu nedenle Hermes, debug esnasında ahead-of-time derlemesi yapmaz ve bytecode’un cihaz üzerinde üretilmesini sağlar. Bu sayede metro bundler gibi araçlarla uygulama geliştirme işlemi oldukça hızlanır. Debug’da bu şekilde çalıştırmanın tek sorunu, prod’da yapılabilecek birtakım optimizasyonların debug’da yapılamamasıdır. Fakat bu optimizasyon eksiklikleri, debug ortamında geliştirim yaparken performans üzerinde çok yüksek bir etkisi bulunmamaktadır.
Standart uyumluluğu
Hermes mevcut durumda ES6 spesifikasyonları ile uyumlı olarak çalışmaktadır. Ayrıca JavaScript spesifikasyonları arttıkça, Hermes de buna bağlı olarak uyum sağlayacaktır. Hermes’in küçük boyutlu halde tutulması için proxy ve eval() gibi JavaScript diline ait birtakım özellikler devre dışı bırakılmıştır.
Hata ayıklama
Debug deneyiminin iyi bir şekilde yapılması için DevTools protokolü aracılığıyla Chrome remote debugging desteği getirilmiştir. Bugüne kadar React Native, debugging desteği için in-app proxy kullanarak Chrome’da JavaScript çalıştırmaya izin vermekteydi. Bu şekilde uygulama debug edilebilse de, React Native bridge’ine giden native senkronize çağrıları desteklenmiyordu. Remote debugging protocol desteği sayesinde, uygulamanın Hermes’e attach edilebilmesi ve bu sayede debug işlemlerinin native olarak gerçekleştirilebilmesi mümkün hale gelmiştir.
React Native için bu iyileştirmelerin etkinleştirilmesi
Hermes’e geçiş sürecindeki eforu azaltmak ve iOS üzerinde JavaScriptCore desteğine devam etmek için JSI (JavaScript Interface) adında bir araç geliştirilmiştir. Bu araç sayesinde bir C++ uygulaması içerisinde herhangi bir JavaScript motorunu embed etmek için lightweight bir API sunmaktadır. Ayrıca bu API ile birlikte, React Native mühendisleri kendi altyapı iyileştirmelerini yapabilir hale gelmişlerdir. Fabric ve TurboModules tarafından da JSI kullanılmaktadır. Fabric, rendering esnasında önceliklendirmeyi sağlamakta, TurboModules ise sadece ihtiyaç duyulan modüllerin lazy olarak yüklenme işlemini gerçekleştirmektedir.
Hermes Hakkında Düşündüklerim
Hermes’i test etmek amacıyla, boş bir proje ve Spotify Lite adında bir RN uygulamasını ele aldım. İki projeyi de hem Hermesli hem de JSC’li olacak şekilde ayarlayıp sonuçları grafiğe döktüm. Şimdi bu sonuçları maddeler halinde inceleyelim.
Uygulama başlatma hızı
Hermes ve JSC’yi karşılaştırdığınızda, boş projede Hermes 192 ms’de açılırken, JSC iki katından daha fazla bir sürede 413ms’de açılıyor. Spotify Lite gibi biraz daha dolu bir uygulamada ise aradaki fark daha hissedilir hale geliyor. Uygulama, Hermes’te 414ms’de açılırken, JSC’de 1 saniyeyi bulabiliyor:
APK boyutuna etkisi
Hermes ve JSC, APK’ya .so dosyaları olarak ekleniyor. libhermes.so dosyası 2MB iken, libjsc.so dosyası 11MB’lık boyutu ile Hermes’in 5 katı kadar alan kaplıyor:
Fakat oluşan bu farkın, dosyanın sıkıştırılmasından dolayı APK’ya etkisinin daha az olduğunu belirtmekte fayda var. Boş projenin boyutu Hermes ile 5.5MB iken, JSC ile 7.9MB oluyor. Yaklaşık 2.4 MB’lık bir kazanç sağlıyor. Spotify Lite gibi bir uygulamada da Hermes ile 9.5MB iken, JSC ile 11.6MB oluyor:
Bundle boyutuna etkisi
Hermes ile bytecode olarak üretilen bundle dosyasının, JS bundle’ına kıyasla çok az miktarda daha büyük olduğunu söyleyebilirim. Boş projede Hermes aktif iken 710 byte olan bundle, JSC’li halinde 654 byte oluyor. Spotify Lite’ta ise Hermes aktif edildiğinde 2195 byte iken, JSC ile 2077 byte oluyor. Yaklaşık 50-100 byte kadar fark oluşuyor. Aradaki fark için denecek kadar az diyebiliriz:
RAM kullanımına etkisi
React Native’deki mimarinin TurboModules ve JSI‘a doğru kayması ile bellek tüketimi konusunda JSC, Hermes ile oldukça başa baş bir mücaadele sergiliyor. Boş projede Hermes aktif iken 81MB olan RAM tüketimi, JSC’li halinde 94MB oluyor. Spotify Lite’ta ise Hermes aktif edildiğinde 196MB iken, JSC ile 229MB oluyor. Bellek tüketimini aşağıdaki komut ile görüntüledim:
adb shell dumpsys meminfo | grep appName
Sonuç olarak
Hermes motoru, kodun parse edilmesi ve bytecode’a dönüştürülmesi işlemlerini derleme esnasında gerçekleştirerek JSC’ye göre daha yüksek bir performans sağlıyor. Dezavantajları ise sadece Android’de kullanılabilmesi ve Reflect ve Proxy gibi bazı özelliklerin bulunamamasından dolayı MobX gibi Reflection kullanan kütüphanelerin düzgün çalışmamasına yol açıyor.
Bu nedenlerden dolayı eğer Hermes’ten çıkarılan özellikleri kullanmıyorsanız ve MobX’e ihtiyacınız yoksa Hermes’i kolaylıkla projenizde çalıştırabilirsiniz.
Bir sonraki yazıda görüşmek üzere…