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