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

Günümüzün web uygulamaları, özellikle de JavaScript kısmı oldukça büyük hale gelebilir. 2018'in ortalarından itibaren HTTP Arşivi, mobil cihazlardaki JavaScript'in ortanca aktarım boyutunu yaklaşık 350 KB'ye yerleştirmektedir. Bu da aktarım boyutudur! JavaScript, ağ üzerinden gönderilirken genellikle sıkıştırılır. Diğer bir deyişle, tarayıcı sıkıştırılmış dosyayı açtıktan sonra JavaScript'in gerçek miktarı biraz daha fazla olur. Kaynak işleme söz konusu olduğunda sıkıştırma önemli olmadığı için bunu belirtmek önemlidir. Sıkıştırılmış JavaScript'in 900 KB boyutu, ayrıştırıcı ve derleyici için hâlâ 900 KB boyutunda olsa da, sıkıştırıldığında yaklaşık 300 KB boyutunda olabilir.

JavaScript'i indirme, açma, ayrıştırma, derleme ve yürütme işlemlerini gösteren bir şema.
JavaScript'i indirme ve çalıştırma işlemi. Komut dosyasının aktarım boyutu sıkıştırılmış 300 KB olsa bile ayrıştırılması, derlenmesi ve yürütülmesi gereken JavaScript'in 900 KB boyutunda olduğunu unutmayın.

JavaScript, işlemesi pahalı bir kaynaktır. İndirildikten sonra göreceli olarak önemsiz kod çözme süresine neden olan görüntülerin aksine, JavaScript ayrıştırılmalı, derlenmeli ve en son olarak yürütülmelidir. Bayt için bayt olarak değerlendirilmesi, JavaScript'i diğer kaynak türlerinden daha pahalı hale getirir.

170 KB JavaScript'lik işlem süresini, eşdeğer boyutlu bir JPEG resmiyle karşılaştıran bir şema. JavaScript kaynağı, JPEG'den çok daha fazla kaynak tüketir.
170 KB JavaScript ayrıştırma/derleme işleminin işleme maliyeti ile eşdeğer boyuttaki bir JPEG'in kodunu çözme işleminin maliyeti. (kaynak).

JavaScript motorlarının verimliliğini iyileştirmek için sürekli olarak iyileştirmeler yapılmaktadır. Bununla birlikte, JavaScript performansını iyileştirmek geliştiriciler için her zamanki gibi bir görevdir.

Bu amaçla, JavaScript performansını iyileştirecek teknikler vardır. Kod bölme, uygulama JavaScript'ini parçalara ayırarak ve bu parçaları yalnızca uygulamanın gerekli olduğu rotalara sunarak performansı artıran bir tekniktir.

Bu teknik işe yaradığı gibi, JavaScript'i yoğun olarak kullanan uygulamaların, yani hiçbir zaman kullanılmayan kodların dahil olduğu yaygın bir sorunu da ortadan kaldırmaz. Ağaç sallama, bu sorunu çözmek için çalışır.

Ağaç sallama nedir?

Ağaç sallama, bir tür ölü kodu ortadan kaldırmaktır. Bu terim Rollup tarafından popüler olmuştu, ancak geçersiz kod eleme kavramı bir süredir zaten kullanılıyordu. Konseptin ayrıca webpack'te de satın alma özelliği bulunmaktadır. Bunlar bu makalede örnek bir uygulama kullanılarak gösterilmiştir.

"Ağaç sallama" terimi, uygulamanızın zihinsel modelinden ve ağaca benzeyen bir yapı olarak bağımlılıklarından 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 statik import ifadeleri aracılığıyla getirilir:

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

Bir uygulama gençse (ya da sadaya) gençse çok az bağımlılığı olabilir. Ayrıca eklediğiniz bağımlılıkların çoğunu (hepsi olmasa da) kullanır. Ancak uygulamanız olgunlaştıkça daha fazla bağımlılık eklenebilir. İşleri bir araya getirmek için eski bağımlılıklar kullanımdan kalksa da kod tabanınızdan yok edilemeyebilir. Sonuçta bir uygulama, çok sayıda kullanılmayan JavaScript ile gönderilir. Ağaç sallama, statik import ifadelerinin ES6 modüllerinin belirli bölümlerini çekme biçiminden yararlanarak bu sorunu çözer:

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

Bu import örneği ile önceki 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ümlerinin içe aktarılmasıdır. Geliştirme derlemelerinde modülün tamamı içe aktarıldığı için bu durum hiçbir şeyi değiştirmez. Üretim derlemelerinde web paketi, açıkça içe aktarılmayan ES6 modüllerinden yapılan dışa aktarmaları "sallayacak" ve böylece bu üretim derlemelerini küçültecek şekilde yapılandırılabilir. Bu kılavuzda bunu nasıl yapacağınızı öğreneceksiniz!

Ağacı sallama fırsatı bulma

Örnek olması açısından, ağaç sallamanın işleyiş şeklini gösteren örnek bir tek sayfalık uygulama sunulmuştur. Bunu klonlayabilir ve isterseniz adımları uygulayabilirsiniz. Ancak bu kılavuzda her adımı birlikte ele alacağız. Bu nedenle, uygulamalı öğrenmeye uygun değilseniz klonlama yapmanız gerekmez.

Örnek uygulama, gitar efekt pedallarının arama yapılabilir bir veritabanıdır. Bir sorgu girdiğinizde efekt pedallarının listesi görünür.

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

Bu uygulamaya yön veren davranış, tedarikçi firmaya (ör. Preact ve Emotion) ve uygulamaya özel kod paketleri (veya webpack'in çağrıştırdığı "parçalar").

Chrome Geliştirici Araçları'nın ağ panelinde gösterilen iki uygulama kodu paketinin (veya parçalarını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; diğer bir deyişle, geliştirme yoluyla optimize edilirler. Uygulamaya özel bir paket için 21,1 KB olması kötü bir durum değildir, ancak ağaç sallantılarının herhangi bir şekilde yaşanmadığını unutmamanız gerekir. Şimdi uygulama koduna bakalım ve bunu düzeltmek için neler yapılabileceğini görelim.

Her 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 şuna benzer bir satır görürsünüz:

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

ES6 modüllerini çeşitli şekillerde içe aktarabilirsiniz ancak buna benzer seçenekler dikkatinizi çekecektir. Bu özel satırda "import utils modülündeki her şey utils adı verilen bir ad alanına yerleştirilir. Burada sorulması gereken asıl soru "bu modülde tam olarak ne kadar öğe var?" sorusudur.

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

Tüm bunlara ihtiyacınız var mı? utils modülünü içe aktaran ana bileşen dosyasında arama yaparak söz konusu ad alanının kaç örneğinin ortaya çıkacağını görmek için bunu tekrar kontrol edelim.

Bir metin düzenleyicide 'utils' aramasının, yalnızca 3 sonuç döndüren ekran görüntüsü.
Tonlarca modülü içe aktardığımız utils ad alanı, ana bileşen dosyasında yalnızca üç kez çağrılır.

utils ad alanı uygulamamızda yalnızca üç noktada yer alıyor. Peki hangi işlev için? Ana bileşen dosyasına tekrar göz atarsanız, yalnızca bir işlev olduğunu görürsünüz. utils.simpleSort, sıralama açılır listeleri değiştirildiğinde arama sonuçları listesini bir dizi ölçüte göre sıralamak için kullanılır:

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);
}

Çok sayıda dışa aktarma işlemi içeren 1.300 satırlık dosyadan yalnızca biri kullanılır. Bu da çok sayıda kullanılmayan JavaScript'in gönderilmesine neden olur.

Bu örnek uygulama biraz yanıltıcı olsa da bu sentetik senaryonun, bir üretim web uygulamasında karşılaşabileceğiniz gerçek optimizasyon fırsatlarına benzediği gerçeğini değiştirmez. Ağaç sallamanın yararlı olabileceği bir fırsat belirlediğinize göre şimdi nasıl yapılıyor?

Babel'in ES6 modüllerini CommonJS modüllerine aktarmasını engelleme

Babel vazgeçilmez bir araçtır ancak ağaç sarsıntısının etkilerini gözlemlemeyi biraz zorlaştırabilir. @babel/preset-env kullanıyorsanız Babel, ES6 modüllerini daha geniş çapta uyumlu CommonJS modüllerine dönüştürebilir. Bu modüller, import yerine require oluşturduğunuz modüllerdir.

CommonJS modülleri için ağaç sallama işlemini yapmak daha zor olduğundan, webpack, bunları kullanmaya karar verirseniz paketlerden neleri ayıklayacağını bilemez. Çözüm, ES6 modüllerini açık bir şekilde olduğu gibi bırakacak şekilde @babel/preset-env yapılandırmasıdır. Babel'i nerede yapılandırdığınıza bağlı olarak (babel.config.js veya package.json içinde olabilir) biraz daha ekleme yapmanız 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ızın bağımlılıklarını sarsırken dikkat etmeniz gereken bir diğer nokta, projenizdeki modüllerin yan etkileri olup olmadığıdır. Yan etkiye örnek olarak, bir işlevin kendi kapsamı dışında bir şeyi değiştirmesi verilebilir. Bu, yürütme işleminin 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ının dışında olan fruits dizisini değiştirdiğinde bir yan etki oluşturuyor.

Yan etkiler ES6 modülleri için de geçerlidir ve ağaç sallama bağlamında önemlidir. Tahmin edilebilir girişler alan ve kendi kapsamlarının dışında hiçbir şeyi değiştirmeden eşit ölçüde öngörülebilir çıktılar üreten modüller, bunları kullanmadığımızda güvenli bir şekilde bırakılabilecek bağımlılıklardır. Bunlar bağımsız, modüler kod parçalarıdır. Bu nedenle "modüller".

Web paketi söz konusu olduğunda projenin package.json dosyasında "sideEffects": false öğesini belirterek paketin ve bağımlılıklarının yan etkiler 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 etkisiz olmadığını bildirebilirsiniz:

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

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

Yalnızca gerekli olanlar içe aktarılıyor

Babel'e ES6 modüllerini olduğu gibi bırakma talimatı verdikten sonra, yalnızca utils modülünden gereken işlevlerin getirilmesi için import söz dizimimizde küçük bir ayarlama yapılması gerekiyor. Bu kılavuz ö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ığından her utils.simpleSort ö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 bunlar olmalıdır. Bağımlılık ağacı sallanmadan önceki web paketi çıkışı şu şekildedir:

                 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şleminden sonra elde edilen çıkış şu şekildedir:

                 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 gerçekten en faydalı main paketi. utils modülünün kullanılmayan kısımları sallandığında main paketi yaklaşık %60 küçülür. Bu, sadece komut dosyasının indirme süresini kısaltmakla kalmaz, aynı zamanda işleme süresini de azaltır.

Gidip ağaç sallayın!

Ağaç sallama işleminden elde ettiğiniz mesafe, uygulamanıza, bağımlılıklarına ve mimarisine bağlıdır. Deneyin. Bu optimizasyonu gerçekleştirmek için modül paketleyicinizi kurmamışsanız, bunu denemeye ve uygulamanıza nasıl fayda sağlayacağını görmeye çalışmanızın bir sakıncası yoktur.

Ağaç sarsıntısı nedeniyle önemli bir performans kazancı elde edebilir veya pek bir fark elde etmemiş olabilirsiniz. Ancak derleme sisteminizi, üretim derlemelerinde bu optimizasyondan yararlanacak şekilde yapılandırıp yalnızca uygulamanızın ihtiyaçlarını seçerek içe aktararak uygulama paketlerinizin proaktif olarak mümkün olduğunca küçük kalmasını sağlarsınız.

Değerli geri bildirimlerinden dolayı Kristofer Baxter, Jason Miller, Addy Osmani, Jeff Posnick, Sam Saccone ve Philip Walton'a teşekkür ederek makalenin kalitesini önemli ölçüde artırdık.