Ağaç sallama ile JavaScript yüklerini azaltın

Günümüzde web uygulamaları, özellikle de JavaScript kısmı oldukça büyük olabilir. 2018'in ortalarından itibaren HTTP Archive, mobil cihazlarda JavaScript'in ortalama aktarım boyutunu yaklaşık 350 KB olarak belirlemiştir. Üstelik bu yalnızca aktarım boyutu. JavaScript genellikle ağ üzerinden gönderilirken sıkıştırılır. Yani JavaScript'in gerçek miktarı, tarayıcı tarafından açıldıktan sonra oldukça fazla olur. Kaynak işleme söz konusu olduğunda sıkıştırmanın alakasız olması nedeniyle bu noktaya dikkat çekmek önemlidir. Sıkıştırılmış haliyle yaklaşık 300 KB olsa bile, sıkıştırılmamış 900 KB JavaScript, ayrıştırıcı ve derleyici için yine 900 KB'tır.

JavaScript'in indirilmesi, sıkıştırmasının çözülmesi, ayrıştırılması, derlenmesi ve yürütülmesi sürecini gösteren bir diyagram.
JavaScript'i indirme ve çalıştırma işlemi. Komut dosyasının aktarım boyutu sıkıştırılmış halde 300 KB olsa bile, ayrıştırılması, derlenmesi ve yürütülmesi gereken 900 KB'lık JavaScript olduğunu unutmayın.

JavaScript'in işlenmesi maliyetlidir. İndirildikten sonra yalnızca nispeten önemsiz bir kod çözme süresi gerektiren resimlerin aksine, JavaScript'in ayrıştırılması, derlenmesi ve ardından yürütülmesi gerekir. Bu durum, bayt başına JavaScript'i diğer kaynak türlerinden daha pahalı hale getirir.

170 KB JavaScript'in işleme süresini eşdeğer boyutta bir JPEG resmin işleme süresiyle karşılaştıran bir şema. JavaScript kaynağı, bayt başına JPEG'den çok daha fazla kaynak kullanır.
170 KB JavaScript'in ayrıştırılması/derlenmesi işleminin maliyeti ile eşdeğer boyutta bir JPEG'in kodunu çözme süresi. (source).

JavaScript motorlarının verimliliğini artırmak için sürekli iyileştirmeler yapılsa da JavaScript performansını artırmak her zaman olduğu gibi geliştiricilerin görevidir.

Bu amaçla, JavaScript performansını artıracak teknikler vardır. Kod bölme, uygulama JavaScript'ini parçalara ayırarak ve bu parçaları yalnızca ihtiyaç duyan bir uygulamanın yollarına sunarak performansı iyileştiren bir tekniktir.

Bu teknik işe yarar ancak JavaScript'in yoğun olarak kullanıldığı uygulamalarda sık karşılaşılan bir sorunu (hiç kullanılmayan kodun dahil edilmesi) çözmez. Ağaç sallama, bu sorunu çözmeye çalışır.

Ağaç sallama nedir?

Ağaç sallama bir tür kod yok etme yöntemidir. Bu terim, Rollup tarafından popülerleştirildi ancak ölü kod kaldırma kavramı uzun zamandır var. Bu kavram, webpack'ta da kullanılmıştır. Bu makalede, örnek uygulama üzerinden bu kavram gösterilmektedir.

"Ağaç sallama" terimi, uygulamanızın ve bağımlılıklarının ağaç benzeri bir yapı olarak zihinsel modelinden gelir. Ağdaki her düğüm, uygulamanız için farklı işlevler sağlayan bir bağımlılıktır. Modern uygulamalarda bu bağımlılıklar aşağıdaki gibi statik import ifadeleri aracılığıyla getirilir:

// Import all the array utilities!
import arrayUtils from "array-utils";

Genç bir uygulamanın (tohum) bağımlılıkları az olabilir. Ayrıca, eklediğiniz bağımlılıkların hepsini olmasa da çoğunu kullanır. Ancak uygulamanız geliştikçe daha fazla bağımlılık eklenebilir. Daha da kötüsü, eski bağımlılıklar kullanımdan kaldırılsa da kod tabanınızdan kaldırılmayabilir. Sonuç olarak bir uygulama, çok sayıda kullanılmayan JavaScript ile birlikte gönderilir. Ağda kaldırma, statik import ifadelerinin ES6 modüllerinin belirli bölümlerini nasıl dahil ettiğinden yararlanarak bu sorunu giderir:

// Import only some of the utilities!
import { unique, implode, explode } from "array-utils";

Bu import örneği ile önceki örnek arasındaki fark, "array-utils" modülündeki her şeyi (çok fazla kod olabilir) içe aktarmak yerine bu örneğin yalnızca belirli bölümlerini içe aktarmasıdır. Modülün tamamı içe aktarıldığından geliştirici derlemelerinde bu durum hiçbir şey değiştirmez. Üretim derlemelerinde webpack, açıkça içe aktarılmayan ES6 modüllerinden dışa aktarma işlemlerini "süzecek" şekilde yapılandırılabilir. Bu sayede üretim derlemeleri daha küçük olur. Bu kılavuzda tam olarak bunu nasıl yapacağınızı öğreneceksiniz.

Ağacı sallamak için fırsatlar bulma

Ağaç sallamanın işleyiş şeklini gösteren örnek bir tek sayfalık uygulama mevcuttur. İsterseniz bu klasörü kopyalayıp adımları takip edebilirsiniz. Ancak bu kılavuzda sürecin her adımını birlikte ele alacağız. Bu nedenle, kopyalama işlemi gerekli değildir (pratik öğrenmeyi tercih etmiyorsanız).

Örnek uygulama, gitar efekt pedallarının aranabilir bir veritabanıdır. Bir sorgu girdiğinizde efekt pedallarının listesi gösterilir.

Gitar efekt pedalı veritabanında arama yapmak için kullanılan örnek bir tek sayfalık uygulamanın ekran görüntüsü.
Örnek uygulamanın ekran görüntüsü.

Bu uygulamayı yönlendiren davranış, tedarikçi (ör. Preact ve Emotion) ve uygulamaya özel kod paketleri (veya webpack'in gerektirdiği "parçalar")

Chrome'un Geliştirici Araçları'nın ağ panelinde gösterilen iki uygulama kodu paketinin (veya parçanın) ekran görüntüsü.
Uygulamanın iki JavaScript paketi. Bunlar sıkıştırılmamış boyutlardır.

Yukarıdaki şekilde gösterilen JavaScript paketleri üretim derlemeleridir. Yani, kod sıkıştırma işlemiyle optimize edilmişlerdir. Uygulamaya özel bir paket için 21,1 KB kötü bir boyut değildir ancak hiç ağaç sallama işlemi yapılmadığı belirtilmelidir. Uygulama koduna bakalım ve bu sorunu düzeltmek için neler yapabileceğimize bakalım.

Her uygulamada, ağaç sallama fırsatları bulmak için statik import ifadeleri aramak gerekecektir. Ana bileşen dosyasının üst kısmında aşağıdaki gibi bir satır görürsünüz:

import * as utils from "../../utils/utils";

ES6 modüllerini çeşitli yollarla içe aktarabilirsiniz ancak buna benzer modüller ilginizi çeker. Bu satırda "import utils modülündeki her şeyi utils adlı bir ad alanına koy" ifadesi yer alır. Burada sorulması gereken önemli soru şudur: "Bu modülde ne kadar şey var?"

utils modülü kaynak koduna bakarsanız yaklaşık 1.300 satır kod olduğunu görürsünüz.

Tüm bu şeylere ihtiyacınız var mı? Bu ad alanının kaç örneğinin bulunduğunu görmek için utils modülünü içe aktaran ana bileşen dosyasını arayarak tekrar kontrol edelim.

Metin düzenleyicide "utils" için yapılan ve yalnızca 3 sonuç döndüren aramanın ekran görüntüsü.
Tonlarca modülü içe aktardığımız utils ad alanı, ana bileşen dosyası içinde yalnızca üç kez çağrılır.

utils ad alanı, uygulamamızda yalnızca üç noktada görünür. Peki hangi işlevler için kullanılır? Ana bileşen dosyasına tekrar bakarsanız yalnızca bir işlev olduğunu görürsünüz. Bu işlev, sıralama açılır listeleri değiştirildiğinde arama sonuçları listesini çeşitli ölçütlere göre sıralamak için kullanılan utils.simpleSort işlevidir:

if (this.state.sortBy === "model") {
  // `simpleSort` gets used here...
  json = utils.simpleSort(json, "model", this.state.sortOrder);
} else if (this.state.sortBy === "type") {
  // ..and here...
  json = utils.simpleSort(json, "type", this.state.sortOrder);
} else {
  // ..and here.
  json = utils.simpleSort(json, "manufacturer", this.state.sortOrder);
}

Bir dizi dışa aktarma işlemi içeren 1.300 satırlık bir dosyadan yalnızca biri kullanılıyor. Bu durum, kullanılmayan çok fazla JavaScript'in yayınlanmasına neden olur.

Bu örnek uygulamanın biraz çelişkili olduğu kabullense de bu sentetik senaryonun, üretime yönelik web uygulamasında karşılaşabileceğiniz gerçek optimizasyon fırsatlarına benzediği gerçeğini değiştirmiyor. Ağaç sallamanın faydalı bir fırsat olduğunu tespit ettiniz. Peki, bu uygulama nasıl yapılır?

Babel'in ES6 modüllerini CommonJS modüllerine dönüştürmesini engelleme

Babel vazgeçilmez bir araçtır ancak ağaç sallamaya bağlı etkilerin gözlemlenmesini biraz daha zorlaştırabilir. @babel/preset-env kullanıyorsanız Babel, ES6 modüllerini daha yaygın uyumlu CommonJS modüllerine (yani import yerine require kullandığınız modüllere) dönüştürebilir.

CommonJS modülleri için ağaç sallamanın yapılması daha zor olduğundan, bunları kullanmaya karar verirseniz webpack hangi paketlerden neleri kaldıracağını bilemez. Bunun çözümü, @babel/preset-env ürününün, ES6 modüllerini açıkça olduğu gibi bırakacak şekilde yapılandırılmasıdır. babel.config.js veya package.json ürününde olması fark etmeksizin Babel'i nerede yapılandırdığınıza bakılmaksızın fazladan bir şeyler eklemeniz gerekir:

// babel.config.js
export default {
  presets: [
    [
      "@babel/preset-env", {
        modules: false
      }
    ]
  ]
}

@babel/preset-env yapılandırmanızda modules: false belirtilmesi, Babel'in istendiği gibi davranmasını sağlar. Bu da webpack'in bağımlılık ağacınızı analiz etmesine ve kullanılmayan bağımlılıkları ortadan kaldırmasına olanak tanır.

Yan etkileri göz önünde bulundurma

Uygulamanızdaki bağımlılıkları kaldırırken dikkate almanız gereken bir diğer husus da projenizin modüllerinin yan etkileri olup olmadığıdır. Yan etkiye örnek olarak bir işlevin kendi kapsamının dışında bir şeyi değiştirmesi verilebilir. Bu durum, yürütme işleminin yan etkisi anlamına gelir:

let fruits = ["apple", "orange", "pear"];

console.log(fruits); // (3) ["apple", "orange", "pear"]

const addFruit = function(fruit) {
  fruits.push(fruit);
};

addFruit("kiwi");

console.log(fruits); // (4) ["apple", "orange", "pear", "kiwi"]

Bu örnekte addFruit, kapsamı dışındaki fruits dizisini değiştirdiğinde bir yan etki oluşturur.

Yan etkiler ES6 modülleri için de geçerlidir ve bu durum ağaç sallama bağlamında önemlidir. Tahmin edilebilir girişler alan ve kendi kapsamları dışında hiçbir şeyi değiştirmeden eşit derecede tahmin edilebilir çıkışlar üreten modüller, kullanılmadıkları takdirde güvenli bir şekilde kaldırılabilecek bağımlılıklardır. Bunlar bağımsız, modüler kod parçalarıdır. Bu nedenle "modüller".

Webpack söz konusu olduğunda, bir projenin package.json dosyasında "sideEffects": false belirtilerek bir paketin ve bağımlılıklarının yan etki içermediğini belirtmek için ipucu kullanılabilir:

{
  "name": "webpack-tree-shaking-example",
  "version": "1.0.0",
  "sideEffects": false
}

Alternatif olarak, hangi belirli dosyaların yan efekt içermeyen olduğunu webpack'e bildirebilirsiniz:

{
  "name": "webpack-tree-shaking-example",
  "version": "1.0.0",
  "sideEffects": [
    "./src/utils/utils.js"
  ]
}

İkinci örnekte, belirtilmeyen dosyaların yan etkisi olmadığı varsayılır. Bu işareti package.json dosyanıza eklemek istemiyorsanız module.rules aracılığıyla webpack yapılandırmanızda da bu işareti belirtebilirsiniz.

Yalnızca gerekli olanları içe aktarma

Babel'e ES6 modüllerini olduğu gibi bırakması için talimat verildikten sonra, yalnızca utils modülünden gereken işlevleri getirmek için import söz dizimimizde küçük bir düzenleme yapılması gerekiyor. Bu kılavuzdaki örnekte, yalnızca simpleSort işlevi olması yeterlidir:

import { simpleSort } from "../../utils/utils";

utils modülünün tamamı yerine yalnızca simpleSort içe aktarıldığı için utils.simpleSort öğesinin her örneğinin simpleSort olarak değiştirilmesi gerekir:

if (this.state.sortBy === "model") {
  json = simpleSort(json, "model", this.state.sortOrder);
} else if (this.state.sortBy === "type") {
  json = simpleSort(json, "type", this.state.sortOrder);
} else {
  json = simpleSort(json, "manufacturer", this.state.sortOrder);
}

Bu örnekte ağaç sallamanın işe yaraması için gereken tek şey budur. Bağımlılık ağacını sallamadan önceki web paketi çıkışı şudur:

                 Asset      Size  Chunks             Chunk Names
js/vendors.16262743.js  37.1 KiB       0  [emitted]  vendors
   js/main.797ebb8b.js  20.8 KiB       1  [emitted]  main

Ağ dallandırma işlemi başarıyla tamamlandıktan sonra elde edilen çıkış şudur:

                 Asset      Size  Chunks             Chunk Names
js/vendors.45ce9b64.js  36.9 KiB       0  [emitted]  vendors
   js/main.559652be.js  8.46 KiB       1  [emitted]  main

Her iki paket de küçülse de en çok avantaj sağlayan main paketidir. utils modülünün kullanılmayan kısımları kaldırılarak main paketi yaklaşık %60 oranında küçültülür. Bu, hem komut dosyasının indirilmesine kadar geçen süreyi hem de işlem süresini kısaltır.

Hadi biraz ağaç sallayın.

Ağaç sallamasından elde edeceğiniz mesafe, uygulamanıza, bağımlılıklarına ve mimarisine bağlıdır. Deneyin! Modül paketleyicinizi bu optimizasyonu gerçekleştirecek şekilde ayarlamadığınızdan eminseniz denemekten ve bunun uygulamanıza nasıl fayda sağladığını görmekten zarar gelmez.

Ağaç sallama işlemi, performansınızda önemli bir artışa yol açabilir veya hiç fark etmeyebilirsiniz. Ancak derleme sisteminizi üretim derlemelerinde bu optimizasyondan yararlanacak şekilde yapılandırarak ve yalnızca uygulamanızın ihtiyaç duyduğu öğeleri seçerek içe aktararak uygulama paketlerinizi proaktif olarak mümkün olduğunca küçük tutabilirsiniz.

Bu makalenin kalitesini önemli ölçüde artıran değerli geri bildirimleri için Kristofer Baxter, Jason Miller, Addy Osmani, Jeff Posnick, Sam Saccone ve Philip Walton'a özel teşekkürler.