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

Günümüzün web uygulamaları, özellikle JavaScript bölümü oldukça büyük olabilir. HTTP Archive, 2018'in ortalarından itibaren mobil cihazlardaki 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ı tarafından sıkıştırması açıldıktan sonra gerçek JavaScript miktarı oldukça fazladır. Kaynak işleme söz konusu olduğunda sıkıştırma işleminin önemi yoktur. Bu nedenle, bu noktayı belirtmek önemlidir. Sıkıştırılmış JavaScript 900 KB'lık bir dosya yaklaşık 300 KB olsa da ayrıştırıcı ve derleyici için 900 KB'tır.

JavaScript'i indirme, sıkıştırmayı açma, ayrıştırma, derleme ve yürütme sürecini gösteren bir diyagram.
JavaScript'in indirilip çalıştırılması süreci. Komut dosyasının aktarım boyutu 300 KB sıkıştırılmış olsa da ayrıştırılması, derlenmesi ve yürütülmesi gereken 900 KB değerinde JavaScript olduğunu unutmayın.

JavaScript'in işlenmesi maliyetli bir kaynaktır. Yalnızca indirildikten sonra nispeten kısa 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, JavaScript'i diğer kaynak türlerine göre daha maliyetli hale getirir.

170 KB JavaScript ile aynı boyuttaki bir JPEG resmin işleme süresini karşılaştıran bir şema. JavaScript kaynağı, bayt bayt JPEG'den çok daha fazla kaynak yoğunlukludur.
170 KB JavaScript'in ayrıştırılmasının/derlenmesinin işleme maliyeti ile eşdeğer boyuttaki bir JPEG'in kod çözme süresi. (kaynak).

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ırmaya yönelik teknikler vardır. Kod bölme, uygulama JavaScript'ini parçalara ayırarak ve bu parçaları yalnızca ihtiyaç duyulan uygulama rotalarına sunarak performansı artıran tekniklerden biridir.

Bu teknik işe yarasa da JavaScript yoğun uygulamaların ortak bir sorunu olan hiç kullanılmayan kodun dahil edilmesi sorununu çözmez. Ağaç sallama, bu sorunu çözmeyi amaçlar.

Ağaç sallama nedir?

Tree shaking, kullanılmayan kodları ortadan kaldırma yöntemidir. Bu terim Rollup tarafından popüler hale getirilmiştir ancak ölü kodları kaldırma kavramı bir süredir mevcuttur. Bu kavram, webpack'te de kullanılmıştır. Bu makalede, örnek bir uygulama aracılığıyla bu durum gösterilmektedir.

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

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

Bir uygulama yeni olduğunda (bir fidan gibi) az sayıda bağımlılığı olabilir. Ayrıca, eklediğiniz bağımlılıkların çoğu (hatta tamamı) kullanılıyor. Ancak uygulamanız geliştikçe daha fazla bağımlılık eklenebilir. İşleri daha da karmaşık hale getiren bir durum da eski bağımlılıkların kullanımdan kalkması ancak kod tabanınızdan temizlenmemesidir. Sonuç olarak, bir uygulama çok sayıda kullanılmayan JavaScript ile birlikte gönderilir. Tree shaking, statik import ifadelerinin ES6 modüllerinin belirli bölümlerini nasıl çektiğinden yararlanarak bu sorunu çözer:

// 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ünden her şeyi (çok fazla kod olabilir) içe aktarmak yerine yalnızca belirli bölümlerin içe aktarılmasıdır. Geliştirme derlemelerinde, modülün tamamı içe aktarıldığından bu durum herhangi bir değişikliğe neden olmaz. Üretim derlemelerinde, webpack, ES6 modüllerinden açıkça içe aktarılmayan dışa aktarmaları "kaldıracak" ş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ğaç sallama fırsatlarını bulma

Tree shaking'in nasıl çalıştığını gösteren örnek bir tek sayfalık uygulama, açıklayıcı amaçlarla kullanılabilir. İsterseniz klonlayıp takip edebilirsiniz ancak bu rehberde tüm adımları birlikte ele alacağız. Bu nedenle, uygulamalı öğrenmeyi tercih etmediğiniz sürece klonlamanız gerekmez.

Ö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 pedalları veritabanında arama yapmak için kullanılan örnek tek sayfalık uygulamanın ekran görüntüsü.
Örnek uygulamanın ekran görüntüsü.

Bu uygulamayı yönlendiren davranış, satıcı (ör. Preact ve Emotion) ve uygulamaya özel kod paketleri (veya webpack'in adlandırdığı şekliyle "parçalar"):

Chrome'un Geliştirici Araçları'ndaki 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. Bu nedenle, karartma yoluyla optimize edilmişlerdir. Uygulamaya özel bir paket için 21, 1 KB kötü bir boyut değildir ancak hiçbir ağaç temizleme işleminin gerçekleşmediği unutulmamalıdır. Uygulama koduna göz atarak bu sorunu düzeltmek için neler yapılabileceğini görelim.

Herhangi bir uygulamada, ağaç sallama fırsatlarını bulmak için statik import ifadeleri aranır. Ana bileşen dosyasının üst kısmına yakın bir yerde şu satırı görürsünüz:

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

ES6 modüllerini çeşitli şekillerde içe aktarabilirsiniz ancak bu gibi modüller dikkatinizi çekmelidir. Bu satırda, "import utils modülündeki her şeyi alıp utils adlı bir ad alanına yerleştir" ifadesi yer alıyor. Burada sorulması gereken önemli soru, "Bu modülde ne kadar şey var?"

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

Bu eşyaların tümüne ihtiyacınız var mı? utils modülünü içe aktaran ana bileşen dosyasını arayarak bu ad alanının kaç örneğinin göründüğünü kontrol edelim.

"utils." için bir metin düzenleyicide yapılan aramanın ekran görüntüsü. Yalnızca 3 sonuç döndürülüyor.
Çok sayıda modül içe aktardığımız utils ad alanı, ana bileşen dosyasında yalnızca üç kez çağrılıyor.

utils ad alanının uygulamamızda yalnızca üç yerde göründüğü ortaya çıktı. Ancak hangi işlevler için? Ana bileşen dosyasına tekrar bakarsanız, 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 tek bir işlev (utils.simpleSort) olduğunu görürsünüz:

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çok dışa aktarma işleminin bulunduğu 1.300 satırlık bir dosyada yalnızca bir dışa aktarma işlemi kullanılır. Bu durum, çok fazla kullanılmayan JavaScript'in gönderilmesine neden olur.

Bu örnek uygulamanın biraz yapay olduğu kabul edilse de bu tür sentetik senaryoların, bir üretim web uygulamasında karşılaşabileceğiniz gerçek optimizasyon fırsatlarına benzediği gerçeğini değiştirmez. Tree shaking'in faydalı 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 aktarmasını engelleme

Babel vazgeçilmez bir araçtır ancak ağaç sallama etkilerini gözlemlemeyi biraz daha zorlaştırabilir. @babel/preset-env kullanıyorsanız Babel, ES6 modüllerini daha yaygın olarak uyumlu CommonJS modüllerine (yani import yerine require modülleri) dönüştürebilir.

Tree shaking, CommonJS modüllerinde daha zor olduğundan bunları kullanmaya karar verirseniz webpack, paketlerden neyin çıkarılacağını bilemez. Çözüm, @babel/preset-env'yı ES6 modüllerini açıkça olduğu gibi bırakacak şekilde yapılandırmaktır. Babel'i nerede yapılandırırsanız yapılandırın (babel.config.js veya package.json), bu işlemde biraz daha fazla şey eklemeniz gerekir:

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

@babel/preset-env yapılandırmanızda modules: false 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ı azaltırken göz önünde bulundurmanız gereken bir diğer nokta da projenizin modüllerinin yan etkileri olup olmadığıdır. Bir yan etki örneği, bir işlevin kendi kapsamı dışındaki bir şeyi değiştirmesidir. Bu, işlevin yürütülmesinin yan etkisidir:

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ışında olan fruits dizisini değiştirdiğinde yan etki oluşturur.

Yan etkiler ES6 modülleri için de geçerlidir ve bu, ağaç temizleme bağlamında önemlidir. Kendi kapsamı dışında hiçbir şeyi değiştirmeden tahmin edilebilir girişler alan ve aynı şekilde tahmin edilebilir çıkışlar üreten modüller, kullanılmadıkları takdirde güvenle bırakılabilecek bağımlılıklardır. Bunlar, bağımsız modüler kod parçalarıdır. Bu nedenle "modüller" olarak adlandırılır.

Webpack söz konusu olduğunda, bir paketin ve bağımlılıklarının yan etkilerden arındırılmış olduğunu belirtmek için projenin package.json dosyasında "sideEffects": false belirtilebilir:

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

Alternatif olarak, webpack'e hangi dosyaların yan etkisiz olmadığını da söyleyebilirsiniz:

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

İkinci örnekte, belirtilmeyen tüm dosyaların yan etkisiz olduğu 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 gerekenleri içe aktarma

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

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

utils modülünün tamamı yerine yalnızca simpleSort içe aktarıldığından utils.simpleSort'nin 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 tree shaking'in çalışması için gereken tek şey budur. Bu, bağımlılık ağacı temizlenmeden önceki webpack çıkışıdır:

                 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

Bu, ağaç temizleme işlemi sonrası çıktısıdır:

                 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 main paketi fayda sağlar. utils modülünün kullanılmayan kısımları kaldırıldığında main paketi yaklaşık %60 oranında küçülür. Bu, komut dosyasının indirilmesi için gereken süreyi ve işleme süresini kısaltır.

Haydi, biraz ağaç salla!

Ağaç sallama işleminden elde edeceğiniz avantajlar, uygulamanıza, bağımlılarına ve mimarisine bağlıdır. Deneyin. Modül paketleyicinizi bu optimizasyonu yapacak şekilde ayarlamadığınızdan eminseniz denemenin ve uygulamanıza nasıl fayda sağladığını görmenin bir sakıncası yoktur.

Tree shaking sayesinde önemli bir performans artışı elde edebilirsiniz veya hiç artış elde 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 bir şekilde 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ürlerimizi sunarız.