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 belirledi. Ü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 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 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ğ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ğ azaltma, 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 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 deyimiyle "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.
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ı? Söz konusu 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ı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ı 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. Çö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, 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, 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ğaç sallama işlemi başarıyla tamamlandıktan sonra elde edilen çıkış:
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.