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, ağ üzerinden gönderilirken genellikle sıkıştırılır. Bu nedenle, tarayıcı JavaScript'i sıkıştırdıktan sonra gerçek JavaScript miktarı 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 maliyetli bir işlemdir. İ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 uygulamanın ihtiyaç duyduğu rotalara sunarak performansı artıran bu tekniklerden biridir.

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ğ sallama nedir?

Ağaç budama, ölü kodun ortadan kaldırılmasına yönelik bir işlemdir. 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, uygulamada çok fazla kullanılmayan JavaScript bulunur. 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. Geliştirme sürümlerinde, modülün tamamı içe aktarıldığı için bu durum hiçbir şeyi değiştirmez. Üretim derlemelerinde webpack, açıkça içe aktarılmayan ES6 modüllerinden dışa aktarmaları "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 deyimiyle "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.

Herhangi bir uygulamada, ağaç sallama fırsatlarını bulmak için statik import ifadeleri aramak gerekir. 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 şekillerde içe aktarabilirsiniz ancak bu tür modüllere dikkat etmeniz gerekir. Bu satırda "import utils modülündeki her şeyi utils adlı bir ad alanına koy" ifadesi yer alır. Burada sorulması gereken en ö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ının ana bileşen dosyasında yalnızca üç kez çağrıldığı görülüyor.

utils ad alanının uygulamamızda yalnızca üç yerde göründüğü anlaşılıyor. Peki bu alan adı hangi işlevler için kullanılıyor? 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 yapay olduğu kabul edilse de bu sentetik senaryonun, üretim aşamasındaki bir web uygulamasında karşılaşabileceğiniz gerçek optimizasyon fırsatlarına benzediği gerçeğini değiştirmez. Ağdaki kodları kaldırmanın yararlı olabileceği bir fırsat belirlediğinize göre bu işlem 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ı etkileri gözlemlemeyi 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. Çözüm, @babel/preset-env'ü ES6 modüllerini açıkça yalnız bırakacak şekilde yapılandırmaktır. Babel'i babel.config.js veya package.json'te yapılandırdığınızda, ek bir işlem yapmanız gerekir:

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

@babel/preset-env yapılandırmanızda modules: false değerini belirtmek, Babel'in istediğiniz 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ı 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. Bir işlevin kendi kapsamının dışındaki bir şeyi değiştirmesi, yan etki olarak adlandırılır. Örneğin:

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, kullanmıyorsak 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, webpack'e hangi dosyaların yan etkisi olmadığını belirtebilirsiniz:

{
 
"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. Bunu package.json dosyanıza eklemek istemiyorsanız bu işareti module.rules aracılığıyla webpack yapılandırmanızda da belirtebilirsiniz.

Yalnızca gerekli olanları içe aktarma

Babel'e ES6 modüllerini kendi haline bırakmasını söyledikten sonra, utils modülünden yalnızca gereken işlevleri getirmek için import söz diziminde küçük bir düzenleme yapılması gerekir. Bu kılavuzun örneğinde, yalnızca simpleSort işlevi gereklidir:

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 çalışması için gereken tek şey budur. Bağımlılık ağacı çalkalanmadan önceki webpack çıkışı:

                 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üçüldüyse de en çok main paketinden yararlanıldı. 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, komut dosyasının indirilmesi için gereken süreyi ve işleme süresini kısaltır.

Hadi biraz ağaç sallayın.

Ağda kaldırma işleminden ne kadar verim alacağınız 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.