Tarayıcılar JavaScript’i Nasıl Yorumlar?

Hangi aşamalardan geçer ve nasıl çalışır?

10 günde yazılmış ve zaman içinde evrimleşerek epey yol katetmiş bu dil bugün milyonlarca web sitesine hayat veriyor. Web, Desktop ve IoT gibi farklı platformlarda hayatına devam ediyor. Eğer bir geliştirici olarak bu yolculuğun nasıl başlayıp nasıl tamamlandığını merak ediyorsanız gelin beraber keyifli bir keşfe çıkalım.🎒✈️

Önce kısa bir bilgi…

JavaScript Brendan Eich tarafından 1995 yılında Netscape’de geliştirildi. İlk önce Mocha adı verildi. Daha sonra LiveScript ve en son JavaScript adını aldı. Netscape Navigator 2 ile hayatımıza girmiş oldu. 1996 yılından itibaren tarayıcılarda görülmeye başladı.

JavaScript EcmaScript dil standardının bir uygulamasıdır. Ecma organizasyonu tarafından 1997 yılında resmi olarak standartlaştırılması kabul edildi. Standardizasyon adı Ecma-262 olarak bilinmekte. Ecma’nın teknik komitesi olan TC39 (Technical Committee 39) çalışma grubu tarafından gelişimi sürdürülmekte. Ecma içerisinde farklı çalışma grupları da bulunuyor. Her çalışma grubu farklı bir işle uğraşıyor. Şuradan liste halinde detaylarıyla görebilirsiniz.


JavaScript tasarım açısından scripting dili olarak tasarlanmıştır. Host environment olarak en çok browser dediğimiz yapı içerisinde çalışıyor. Ancak, JavaScript yorumlanan bir dil ve yorumlayıcının olduğu her ortamda çalışabilir. Bkz: Nodejs, Electron. Bizim bu yazıda çıktığımız yolculuk bu yorumlayıcıları anlamak üzerine olacak.

Biz geliştiriciler olarak JavaScript’i en çok browser üzerinde çalıştırıyoruz. Elbette bu dilin çalışabilmesi için bazı yorumlayıcılara ihtiyaç duyuluyor. Günümüzde browser denince akla gelen Chrome, Firefox, Safari, Internet Explorer, Opera gibi uygulamalar JavaScript’i yorumlamak için adına Motor (Engine) denilen araçlar kullanıyorlar.

JavaScript motorları yaptığımız işin kalbinde yer alıyor.

Öncelikle araba motorlarını hatırlayalım. Araba motorları belli bir işlevi yerine getirmek için tasarlanmış aracın en önemli parçasıdır. Aracın hareket edebilmesi için yakıtın hareket enerjisine dönüştürülmesi gerekmektedir. İşte bu hareket enerjisini üretmek için motor içinde yakıtın yakılması gerekli. Bu sayede motor arabaya hareket kazandıracak enerjiyi üretebilir.

Kısa bir motor bilgisinden sonra konumuza dönersek aslında tarayıcılar içerisindeki JavaScript yorumlayıcılarına neden motor denildiğini daha iyi anlayabilmiş olmanızı ümit ediyorum. Hatta Chrome’un JavaScript yorumlayıcısı olan V8 Moturunun adı V şeklinde dizayn edilmiş ve 8 silindirden oluşan bir motor türünden esinlenilmiştir. Bu motor türü iyi performansı ve çok yakıt yakmasıyla bilinir. Sanırım Chrome için en doğru motor adını seçmişler.

Bir süre öncesine kadar Chrome’un fazla kaynak tüketiminden oldukça şikayetçi bir kitle mevcuttu (bunlardan biri benim). Bunun önemli bir bölümüne V8 motorunun neden olduğu anlaşıldı. En son yapılan iyileştirilmelerle bu kaynak tüketimi düşüşe geçti. 😊

Tarayıcılara Göre Kullanılan JavaScript Motorları

  • Chrome — V8
  • Firefox — SpiderMonkey
  • IE — Chakra
  • Safari — JavaScriptCore
Ufak bir dipnot: SpiderMonkey geliştirilen ilk JavaScript motorudur ve JavaScript dilinin mucidi Brendan Eich tarafından C dili ile yazılmıştır. Yani Firefox’tan önce Netscape içerisinde çalışıyordu.

JavaScript motorlarını aslında bir tür sanal makine olarak adlandırabiliriz. JavaScript kodunu yorumlamak ve çalıştırmak için özel olarak tasarlanmış bir tür “process virtual machine”’dir (kısaca PVM diyelim biz). Process virtual machine aslında tek bir yazılımı çalıştırmak için dizayn edilmiş sanal makineyi ifade eder. Örneğin: Linux sistemler üzerinde Windows programlarını çalıştırmak için kullanılan Wine gibi. JavaScript motorlarının tek amacı kodu okuyup derlemek olduğu için bu tanıma girmesi oldukça uygun sanırım. Bir de bunun “system virtual machine” (kısaca SVM diyelim biz) olanı var. Örnek olarak: MacOSX üzerinde VirtualBox ile windows işletim sisteminin çalıştırılması gibi.

Her JavaScript motoru EcmaScript sürümlerini kendisine implemente eder. EcmaScript geliştikçe bu motorlar da kendilerini buna göre güncelleyerek dilin yeni özelliklerinin kullanılabilmesinin önünü açarlar. Tarayıcılar bu implementasyonu yapsa bile eski sürümleri bu özellikleri desteklemeyebilir. Bu gibi durumlarda yazdığımız kodu transpiler dediğimiz araçlardan geçiririz. Yaygın olarak kullanılan tüm tarayıcıların JavaScript motorları ile uyumlu olan EcmaScript versiyonunu elde edip tarayıcıya bu kodu çalıştırmasını söyleriz. Böylelikle geriye dönük uyumluluk problemleri ile daha az uğraşırız. Bu transpiler araçlarının en bilineni ve yaygın olarak kullanılanı ise Babel.

JavaScript motorunu ifade ederken kullandığımız oku ve derle prensibi bu motorların basit olduklarını yansıtmaz. Örneğin JavaScript motorları analiz, yorumlama, optimizasyon ve garbage collection gibi yapılara sahipler. Dolayısı ile bir JavaScript kodunun çalışabilir bir koda dönüştürülmesi sırasında bir dizi işlemden geçtiğini söyleyebiliriz. Yani aslında haberiniz olmadan arkada detaylı bir çalışma yapılıyor.

Biz geliştiriciler olarak çoğu zaman bu arkada dönen filmin farkında olmayız veya önemsemeyiz. Ancak iyi bir geliştirici olmak, yazdığınız kodun çalıştığı ortamda nasıl bir süzgeçten geçtiğini ve nasıl bir tepkimeye neden olduğu hakkında fikir sahibi olmayı gerektirir. Bu sayede daha performanslı kodları nasıl yazacağınıza dair bilgi sahibi olabilirsiniz. Bu yüzden aslında çok önemlidir. 🙂

JavaScript motorları farklı şekillerde dizayn edildikleri için çalışma şekilleri birbirinden kısmen farklı olabiliyor. Ben biraz V8 özelinde anlatmaya çalışacağım konuyu çünkü son zamanlarda V8 üzerinde bir şeyler geliştiriyorum ve ilerleyen zamanlarda bununla ilgili de bir yazı hazırlayacağım.

V8 c++ ile yazılmış ve Chrome içerisinde kullanılan yüksek performanslı bir JavaScript motoru. V8'in güzel yanı tarayıcıdan bağımsız olarak tek başına çalıştırılabilmesi. Bu özellik beraberinde farklı gelişmelere yol açıyor ve Node.js, Electron gibi JavaScript’in farklı platformlarda çalışabilmesini sağlıyor. Diğer yandan Node.js için V8 dışında node-chakracore ve SpiderNode projeleri halihazırda kullanılabiliyor.

Peki V8'in pipeline’ı bugüne nasıl geldi bir bakalım mı?

2008 İlk Compiler Pipeline

Görsel: Leszek Swirski & Ross McIlroy — Google

JavaScript kodu önce Parser’dan geçiyor. Bu işlemin sonucunda AST (Abstract Syntax Tree)’ye çevriliyor. Daha sonra Codegen AST kodunu alıp kendince biraz optimize edip makine koduna dönüştürüyor. Üç aşamalı basit bir çalışma şekline sahip.

Parser

Tanım olarak basit tabir ile anlatırsak normal konuşma dillerinde nasıl anlaşılabilir olmak için cümlelerimizi oluşturan sözcükleri bir gramer yapısına ve kurallar bütününe göre diziyorsak bilgisayar dillerinde de durum aynı. Sadece tek farkla aynılar. Bilgisayar dilleri konuşma diline göre çok daha karmaşık yapılara sahipler.

Kısacası parser yazdığımız kodları kurallara uygun olarak (syntax) ufak parçalara bölüp işlenmesi için sıraya alır, kodu tanır ve üzerine düşen işlemi tamamladıktan sonra işlemi AST devralır.

🌲 AST (Abstract Syntax Tree) — Soyut Sözdizimi Ağacı

Öncelikle kod sözcüksel analiz (lexical analysis) ve sembolleri gruplama (tokenizing) işlemine alınır.

Daha iyi anlamak için basit bir örnek yapalım;

const ast = “Oğuz”;

JavaScript kodundan elde ettiğimiz AST çıktısı şu şekilde;

astexplorer.net

AST kodunu alan derleyici (burada Codegen) yine bazı iyileştirmeler yaparak direkt olarak makine koduna dönüştürür.

Burada bir parantez açıp Tree Shaking konusuna değinmek istiyorum. Tree Shaking yazdığımız kodların içerisindeki kullanılmayan kodların çıkarılması/elimine edilmesi için kullanılan bir terim. Özellikle import/export modülleri sayesinde webpack gibi bundler araçları yukarıdaki AST çıktısına bakarak kullanmadığınız kodları atabiliyor. Bu bize önemli bir performans avantajı sağlıyor.
Tree Shaking: temsili

Zaman içerisinde farklı performans ihtiyaçları nedeniyle bu anlattığım pipeline evrimleşti. Hatta 2016'da epey bi karıştırdılar işleri. Kafa karışıklığı yaratmamak adına geçen sürede nasıl evrimleştiğini anlatmıyorum.

Sonuç olarak 2017'ye gelindiğinde pipeline olması gerektiği şekilde daha sade ve daha performanslı bir hale dönüştürüldü. Şu an Chrome’un yani V8'in JavaScript’i yorumlama şekli kısaca şöyle;

2017 Son Compiler Pipeline

Görsel: Leszek Swirski & Ross McIlroy — Google

Bu pipeline’da JavaScript kodu parse ediliyor ve ardından AST oluşturuluyor. Ignition (TurboFan altyapısı ile oluşturulmuş yeni bir interpreter) oluşan AST üzerinden Bytecode’u ürettikten sonra TurboFan (JIT derleyici) üzerinde optimize edilerek makine koduna dönüştürülüyor.

Şimdi burada süre gelen zaman içerisinde neden direkt makine koduna compile edilirken bytecode üretmeye başladılar diye düşünüyor olabilirsiniz. V8 ekibi bunun en büyük nedenlerinden birinin fazla kaynak tüketimini azaltmak olduğunu belirtiyorlar.

TurboFan ne yapıyor?

Oluşan bytecode sonrası TurboFan yavaş çalışan bir şeyler var mı ve bunları optimize etmek gerekiyor mu veya bir tıkanıklık var mı diye kontrol ediyor. Örneğin fazla kaynak tüketen bir fonksiyon TurboFan yardımı ile optimize edilerek makine koduna çevriliyor. Bu sürecin takibi ve analiz edilip yeniden optimize edilmesi elbette ekstra bir kaynak tüketimine neden oluyor. Özellikle mobil cihazlarda çalışan Chrome’un daha fazla pil tüketimine neden olan sebeplerden bir tanesi bu analiz sürecinde harcanan ekstra kaynak.

Bu tarz mekanizmalar JavaScript motorlarının aslında performans odaklı gelişiminin bir sonucu. Yazdığınız kodun daha hızlı çalıştırılabilmesi için haberiniz olmadan işleyen bir süreçler bütünü hepsi.


Concurrency Model

Görsel: MSDN

JavaScript motorları bir tür modele dayalı uygulamalar gerçekleştirir ve aynı zamanda bunları optimize etmeye çalışırlar. Şimdi bunu biraz açalım;

Memory Heap adından da anlaşılacağı gibi bellek ayırma işleminin gerçekleştiği yeri ifade ediyor.

Call Stack ise o an nerede olduğumuzu kaydeden bir veri yapısı olarak özetlenebilir. Her girdi Stack Frame olarak adlandırılmaktadır. Buraya sürekli veri eklenebilir (Push) ve çıkarılabilir (Pop). Stack’ler LIFO yapısında çalışırlar. Yani son giren ilk çıkar mantığı uygulanır. JavaScript single-thread bir dil olduğu için tek bir call-stack’e sahip. Bunun anlamı her defasında tek bir şey yapılabilmektedir.

Queue ise kuyruk anlamına gelmektedir. JavaScript runtime’ında işlenecek iletilerin bir listesi tutulur ve sıra ile işleme alınır. Bu bilgiyi tutan kısıma Queue diyoruz. Stack’lerin aksine burada FIFO yapısı uygulanır. Yani ilk giren ilk çıkar.

Stack konusu açılmışken birde stack-overflow konusuna değinelim çok kısaca.

Çıkış noktası olmayan bir kod; örneğin kendini çağıran fonksiyon / sonsuz döngü stack’te yer işgal etmeye devam ettikçe stack-size dolar ve stack-overflow durumu ile karşılaşırız. Dolayısı ile recursive çağrılarda sonsuz döngüye girmemesine dikkat etmemiz gerekir.

Aslında çok basit ifade ile anlatırsak bir kovaya alabileceğinden fazla su doldurulması sonucu suyun dışarı taşması durumu olarak özetlenebilir.

Bir örnek yaparak açıklayalım;

function stackOverflow(){
stackOverflow();
}
stackOverflow();
Yukarıdaki kodun console çıktısı

Garbage Collection

JavaScript performansının artırılması ve sorunsuz çalışması için bellek yönetiminin yapılması oldukça önemli bir konu. V8 motoru bu işlemi dinamik olarak yapmaktadır. Periyodik olarak JavaScript uygulaması için ayırılan belleğin üzerinden geçerek ihtiyaç duyulmayan veriye ait bellek temizlenir. JavaScript’in belleğe erişmek için bir arayüzü bulunmuyor ve browser tarafından bir API sağlanmıyor. Dolayısı ile bellek yönetimi tümüyle V8 motoru tarafından kontrol ediliyor. V8 bunu dinamik olarak denetleyip bellek performansını en iyi şekilde optimize etmek için çalışıyor.


JavaScript motorlarının çalışma prensiplerini bilerek kod yazmak uzun vadede daha fazla performansa dayalı ürünler ortaya çıkarmak için bence önemli bir nokta. Yazdığınız kodun bir tepkime yaratırken hangi aşamalardan geçtiğini bilmek daha tutarlı ve hızlı sonuç almanızı sağlar.

Tarayıcı yarışında JavaScript motorları önemli bir paya sahip. Ben her ne kadar V8 üzerinden anlatmış olsamda çoğu motorun çalışma prensibi birbirine yakın. Sadece yorumlama şekilleri farklı.

Uzun zamandır tarayıcılar optimize ettikleri motorlar ile JavaScript’in, dolayısı ile web sitelerinin daha hızlı çalışması için çaba sarf ediyorlar. Ancak, elbette performansın önemli bir bölümünün yine geliştiricilerin ellerinde olduğunu unutmayalım. Bkz: Optimization Killers

Yazıyı beğendiyseniz paylaşmayı ve beğenmeyi unutmayın.

Bana her konuda oguzz.kilic@gmail.com mail adresinden ulaşabilirsiniz.

Sevgiler.

Faydalı Kaynaklar