Ağaç Sarsıntısı: Bir Başvuru Kılavuzu

Yayınlanan: 2022-03-10
Kısa özet ↬ "Ağaç sallama", JavaScript'i paketlerken sahip olunması gereken bir performans optimizasyonudur. Bu makalede, tam olarak nasıl çalıştığı ve özelliklerin ve uygulamanın demetleri daha yalın ve daha performanslı hale getirmek için nasıl iç içe geçtiği konusunda daha derine iniyoruz. Ayrıca, projeleriniz için kullanmak üzere ağaç sallayan bir kontrol listesi alacaksınız.

Ağaç sallamanın ne olduğunu ve bununla başarıya nasıl hazırlanacağımızı öğrenmek için yolculuğumuza başlamadan önce, JavaScript ekosisteminde hangi modüllerin olduğunu anlamamız gerekiyor.

JavaScript programlarının ilk günlerinden beri karmaşıklığı ve gerçekleştirdikleri görevlerin sayısı arttı. Bu tür görevleri kapalı yürütme kapsamlarına ayırma ihtiyacı ortaya çıktı. Bu görev bölümleri veya değerler, modüller olarak adlandırdığımız şeydir. Ana amaçları, tekrarı önlemek ve yeniden kullanılabilirlikten yararlanmaktır. Bu nedenle, mimariler bu tür özel kapsam türlerine izin vermek, değerlerini ve görevlerini ortaya çıkarmak ve dış değerleri ve görevleri tüketmek için tasarlandı.

Modüllerin ne olduğuna ve nasıl çalıştıklarına daha derinlemesine dalmak için “ES Modülleri: Bir Karikatür Derin Dalışı”nı öneriyorum. Ancak ağaç sallamanın ve modül tüketiminin nüanslarını anlamak için yukarıdaki tanım yeterli olacaktır.

Ağaç Sarsıntısı Aslında Ne Anlama Geliyor?

Basitçe söylemek gerekirse, ağaç sallama, erişilemeyen kodun (ölü kod olarak da bilinir) bir paketten çıkarılması anlamına gelir. Webpack sürüm 3'ün belgelerinde belirtildiği gibi:

“Uygulamanızı bir ağaç olarak hayal edebilirsiniz. Gerçekte kullandığınız kaynak kodu ve kitaplıklar, ağacın yeşil, canlı yapraklarını temsil eder. Ölü kod, ağacın sonbaharda tüketilen kahverengi, ölü yapraklarını temsil eder. Ölü yapraklardan kurtulmak için ağacı sallayarak onların düşmesini sağlamalısınız.”

Terim ilk olarak ön uç toplulukta Rollup ekibi tarafından popüler hale getirildi. Ancak tüm dinamik dillerin yazarları çok daha önceden beri sorunla mücadele ediyor. Bir ağaç sallama algoritması fikri, en azından 1990'ların başına kadar izlenebilir.

JavaScript ülkesinde, daha önce ES6 olarak bilinen ES2015'teki ECMAScript modülü (ESM) spesifikasyonundan bu yana ağaç sallamak mümkün olmuştur. O zamandan beri, programın davranışını değiştirmeden çıktı boyutunu küçülttükleri için, çoğu paketleyicide ağaç sallama varsayılan olarak etkinleştirilmiştir.

Bunun temel nedeni, ESM'lerin doğası gereği statik olmasıdır. Bunun ne anlama geldiğini inceleyelim.

Atlamadan sonra daha fazlası! Aşağıdan okumaya devam edin ↓

ES Modülleri ve CommonJS karşılaştırması

CommonJS, ESM belirtiminden birkaç yıl önce gelir. JavaScript ekosistemindeki yeniden kullanılabilir modüller için destek eksikliğini gidermek için ortaya çıktı. CommonJS, sağlanan yola dayalı olarak harici bir modül getiren bir request require() işlevine sahiptir ve bunu çalışma zamanı sırasında kapsama ekler.

Bu require , bir programdaki diğerleri gibi bir function , derleme zamanında çağrı sonucunu değerlendirmeyi yeterince zorlaştırır. Bunun da ötesinde, kodun herhangi bir yerine require çağrıları eklemek mümkündür - başka bir işlev çağrısına, if/else deyimlerine, switch deyimlerine vb.

CommonJS mimarisinin geniş çapta benimsenmesinden kaynaklanan öğrenme ve mücadelelerle, ESM belirtimi, modüllerin ilgili import ve export anahtar sözcükleri tarafından içe ve dışa aktarıldığı bu yeni mimariye yerleşti. Bu nedenle, artık işlevsel çağrılar yok. ESM'lere yalnızca üst düzey bildirimler olarak da izin verilir - statik oldukları için başka bir yapıya yerleştirmek mümkün değildir: ESM'ler çalışma zamanı yürütmesine bağlı değildir.

Kapsam ve Yan Etkiler

Bununla birlikte, ağaç sallamanın şişkinlikten kaçınmak için üstesinden gelmesi gereken başka bir engel daha vardır: yan etkiler. Bir işlevin, yürütme kapsamı dışındaki faktörleri değiştirdiği veya bunlara dayandığı zaman yan etkileri olduğu kabul edilir. Yan etkileri olan bir işlev, saf olmayan olarak kabul edilir. Saf bir işlev, bağlam veya çalıştırıldığı ortamdan bağımsız olarak her zaman aynı sonucu verir.

 const pure = (a:number, b:number) => a + b const impure = (c:number) => window.foo.number + c

Paketleyiciler, bir modülün saf olup olmadığını belirlemek için sağlanan kodu mümkün olduğunca değerlendirerek amaçlarına hizmet eder. Ancak derleme süresi veya paketleme süresi sırasında kod değerlendirmesi ancak bir yere kadar gidebilir. Bu nedenle, yan etkileri olan paketlerin, tamamen ulaşılmaz olsa bile, düzgün bir şekilde ortadan kaldırılamayacağı varsayılmaktadır.

Bu nedenle, paketleyiciler artık modülün package.json dosyasında, geliştiricinin bir modülün hiçbir yan etkisinin olup olmadığını bildirmesine olanak tanıyan bir anahtarı kabul eder. Bu şekilde geliştirici, kod değerlendirmesini devre dışı bırakabilir ve paketleyiciye ipucu verebilir; belirli bir paket içindeki kod, ona bağlanan erişilebilir bir içe aktarma veya require ifadesi yoksa elimine edilebilir. Bu, yalnızca daha yalın bir paket oluşturmakla kalmaz, aynı zamanda derleme sürelerini de hızlandırabilir.

 { "name": "my-package", "sideEffects": false }

Bu nedenle, bir paket geliştiriciyseniz, yayınlamadan önce sideEffects dikkatli bir şekilde kullanın ve tabii ki beklenmedik kırılma değişikliklerinden kaçınmak için her sürümde revize edin.

Kök sideEffects anahtarına ek olarak, yöntem çağrınıza /*@__PURE__*/ satır içi bir açıklama ekleyerek dosya bazında saflığı belirlemek de mümkündür.

 const x = */@__PURE__*/eliminated_if_not_called()

Bu satır içi açıklamanın, bir paketin sideEffects: false beyan etmemesi durumunda veya kitaplığın gerçekten belirli bir yöntem üzerinde bir yan etki sunması durumunda yapılması gereken tüketici geliştiricisi için bir kaçış kapısı olduğunu düşünüyorum.

Web Paketini Optimize Etme

Sürüm 4'ten itibaren Webpack, en iyi uygulamaların çalışmasını sağlamak için giderek daha az yapılandırma gerektirdi. Birkaç eklentinin işlevselliği çekirdeğe dahil edilmiştir. Ve geliştirme ekibi paket boyutunu çok ciddiye aldığı için ağaç sallamayı kolaylaştırdılar.

Çok fazla tamirci değilseniz veya uygulamanızın özel bir durumu yoksa, bağımlılıklarınızı ağaç sarsmak sadece bir satır meselesidir.

webpack.config.js dosyası, mode adlı bir kök özelliğine sahiptir. Bu özelliğin değeri production olduğunda, ağaç sarsılır ve modüllerinizi tamamen optimize eder. TerserPlugin ile ölü kodu ortadan kaldırmanın yanı sıra, mode: 'production' , modüller ve parçalar için deterministik karışık adları etkinleştirecek ve aşağıdaki eklentileri etkinleştirecektir:

  • bayrak bağımlılığı kullanımı,
  • bayrak dahil parçalar,
  • modül birleştirme,
  • hata yaymaz.

Tetikleyici değerin production olması tesadüf değildir. Sorunları hata ayıklamayı çok daha zor hale getireceğinden, bir geliştirme ortamında bağımlılıklarınızın tam olarak optimize edilmesini istemeyeceksiniz. Bu yüzden iki yaklaşımdan biriyle devam etmenizi öneririm.

Bir yandan, Webpack komut satırı arayüzüne bir mode bayrağı iletebilirsiniz:

 # This will override the setting in your webpack.config.js webpack --mode=production

Alternatif olarak, webpack.config.js içindeki process.env.NODE_ENV değişkenini kullanabilirsiniz:

 mode: process.env.NODE_ENV === 'production' ? 'production' : development

Bu durumda, dağıtım işlem hattınızda --NODE_ENV=production iletmeyi unutmamalısınız.

Her iki yaklaşım da Webpack sürüm 3 ve altındaki çok bilinen definePlugin üstünde bir soyutlamadır. Hangi seçeneği seçtiğiniz kesinlikle hiçbir fark yaratmaz.

Web Paketi Sürüm 3 ve Aşağısı

Bu bölümdeki senaryoların ve örneklerin Webpack ve diğer paketleyicilerin son sürümleri için geçerli olmayabileceğini belirtmekte fayda var. Bu bölüm, Terser yerine UglifyJS sürüm 2'nin kullanımını ele almaktadır. UglifyJS, Terser'in çatallandığı pakettir, bu nedenle kod değerlendirmesi aralarında farklılık gösterebilir.

Webpack sürüm 3 ve altı, package.json içindeki sideEffects özelliğini desteklemediğinden, kod elimine edilmeden önce tüm paketlerin tamamen değerlendirilmesi gerekir. Bu tek başına yaklaşımı daha az etkili kılar, ancak birkaç uyarının da dikkate alınması gerekir.

Yukarıda bahsedildiği gibi, derleyicinin bir paketin global kapsamı ne zaman kurcaladığını kendi başına bulmasının bir yolu yoktur. Ancak ağaç sallamayı atladığı tek durum bu değil. Daha bulanık senaryolar var.

Bu paket örneğini Webpack'in belgelerinden alın:

 // transform.js import * as mylib from 'mylib'; export const someVar = mylib.transform({ // ... }); export const someOtherVar = mylib.transform({ // ... });

Ve işte bir tüketici paketinin giriş noktası:

 // index.js import { someVar } from './transforms.js'; // Use `someVar`...

mylib.transform yan etkileri başlatıp başlatmadığını belirlemenin bir yolu yoktur. Bu nedenle, hiçbir kod ortadan kaldırılmayacaktır.

Benzer bir sonuca sahip diğer durumlar şunlardır:

  • derleyicinin denetleyemediği bir üçüncü taraf modülden bir işlevi çağırmak,
  • üçüncü taraf modüllerden içe aktarılan işlevleri yeniden dışa aktarma.

Derleyicinin ağaç titremesini çalıştırmasına yardımcı olabilecek bir araç, babel-plugin-transform-imports'tur. Tüm üye ve adlandırılmış dışa aktarmaları varsayılan dışa aktarmalara bölerek modüllerin ayrı ayrı değerlendirilmesine olanak tanır.

 // before transformation import { Row, Grid as MyGrid } from 'react-bootstrap'; import { merge } from 'lodash'; // after transformation import Row from 'react-bootstrap/lib/Row'; import MyGrid from 'react-bootstrap/lib/Grid'; import merge from 'lodash/merge';

Ayrıca, geliştiriciyi zahmetli içe aktarma ifadelerinden kaçınması için uyaran bir yapılandırma özelliğine de sahiptir. Web Paketi sürüm 3 veya üzerindeyseniz ve temel yapılandırma konusunda gerekli özeni gösterdiyseniz ve önerilen eklentileri eklediyseniz, ancak paketiniz hala şişkin görünüyorsa, bu paketi denemenizi öneririm.

Kapsam Kaldırma ve Derleme Süreleri

CommonJS zamanında, çoğu paketleyici her modülü başka bir işlev bildirimi içine sarar ve bunları bir nesnenin içinde eşler. Bu, dışarıdaki herhangi bir harita nesnesinden farklı değil:

 (function (modulesMap, entry) { // provided CommonJS runtime })({ "index.js": function (require, module, exports) { let { foo } = require('./foo.js') foo.doStuff() }, "foo.js": function(require, module, exports) { module.exports.foo = { doStuff: () => { console.log('I am foo') } } } }, "index.js")

Statik olarak analiz etmenin zor olmasının yanı sıra, bu temelde ESM'ler ile uyumsuzdur, çünkü import ve export ifadelerini saramayacağımızı gördük. Bu nedenle, günümüzde paketleyiciler her modülü en üst seviyeye taşıyor:

 // moduleA.js let $moduleA$export$doStuff = () => ({ doStuff: () => {} }) // index.js $moduleA$export$doStuff()

Bu yaklaşım, ESM'ler ile tamamen uyumludur; artı, kod değerlendirmesinin çağrılmayan modülleri kolayca tespit etmesine ve bunları bırakmasına olanak tanır. Bu yaklaşımın uyarısı, derleme sırasında çok daha fazla zaman almasıdır, çünkü işlem sırasında her ifadeye dokunur ve paketi bellekte depolar. Bu, paketleme performansının herkes için daha büyük bir endişe haline gelmesinin ve web geliştirme araçlarında derlenmiş dillerin kullanılmasının büyük bir nedenidir. Örneğin, esbuild Go'da yazılmış bir paketleyicidir ve SWC, Rust'ta yazılmış ve yine Rust'ta yazılmış bir paketleyici olan Spark ile entegre olan bir TypeScript derleyicisidir.

Kapsam kaldırmayı daha iyi anlamak için Parsel sürüm 2'nin belgelerini şiddetle tavsiye ederim.

Erken Nakilden Kaçının

Ne yazık ki oldukça yaygın olan ve ağaç sarsıntısı için yıkıcı olabilen belirli bir sorun var. Kısacası, özel yükleyicilerle çalışırken, paketleyicinize farklı derleyiciler entegre ettiğinizde olur. Tüm olası permütasyonlarda yaygın kombinasyonlar TypeScript, Babel ve Webpack'tir.

Hem Babel hem de TypeScript'in kendi derleyicileri vardır ve ilgili yükleyicileri, kolay entegrasyon için geliştiricinin bunları kullanmasına izin verir. Ve burada gizli tehdit yatıyor.

Bu derleyiciler, kod optimizasyonundan önce kodunuza ulaşır. Ve ister varsayılan ister yanlış yapılandırma olsun, bu derleyiciler genellikle ESM'ler yerine CommonJS modülleri üretir. Önceki bölümde bahsedildiği gibi, CommonJS modülleri dinamiktir ve bu nedenle ölü kodların ortadan kaldırılması için uygun şekilde değerlendirilemez.

Bu senaryo, "izomorfik" uygulamaların (yani hem sunucu hem de istemci tarafında aynı kodu çalıştıran uygulamalar) büyümesiyle günümüzde daha da yaygın hale geliyor. Node.js henüz ESM'ler için standart desteğe sahip olmadığından, derleyiciler node ortamını hedeflediğinde CommonJS çıktısı verirler.

Bu nedenle, optimizasyon algoritmanızın aldığı kodu kontrol ettiğinizden emin olun.

Ağaç Sallama Kontrol Listesi

Artık donatmanın ve ağaç sallamanın nasıl çalıştığının ayrıntılarını bildiğinize göre, mevcut uygulamanızı ve kod tabanınızı tekrar ziyaret ettiğinizde kullanışlı bir yere yazdırabileceğiniz bir kontrol listesi hazırlayalım. Umarız bu size zaman kazandırır ve yalnızca kodunuzun algılanan performansını değil, hatta belki işlem hattınızın yapım sürelerini de optimize etmenize olanak tanır!

  1. ESM'leri kullanın ve yalnızca kendi kod tabanınızda değil, aynı zamanda sarf malzemeleri olarak ESM çıktısı veren paketleri de tercih edin.
  2. Bağımlılıklarınızdan hangisinin (varsa) tam olarak sideEffects veya bunları true olarak ayarladığını bildiğinizden emin olun.
  3. Yan etkileri olan paketleri tüketirken saf olan yöntem çağrılarını bildirmek için satır içi açıklamalardan yararlanın.
  4. CommonJS modüllerinin çıktısını alıyorsanız, içe aktarma ve dışa aktarma ifadelerini dönüştürmeden önce paketinizi optimize ettiğinizden emin olun.

Paket Yazma

Umarım, bu noktada hepimiz ESM'lerin JavaScript ekosisteminde ileriye giden yol olduğu konusunda hemfikiriz. Yazılım geliştirmede her zaman olduğu gibi, geçişler zor olabilir. Neyse ki, paket yazarları, kullanıcıları için hızlı ve sorunsuz geçişi kolaylaştırmak için kesintisiz önlemler alabilir.

package.json dosyasına yapılan bazı küçük eklemelerle, paketiniz paketleyicilere paketin desteklediği ortamları ve bunların en iyi nasıl desteklendiğini söyleyebilecektir. İşte Skypack'ten bir kontrol listesi:

  • Bir ESM dışa aktarımını dahil edin.
  • "type": "module" ekleyin.
  • "module": "./path/entry.js" (bir topluluk kuralı).

İşte tüm en iyi uygulamalar izlendiğinde ve hem web hem de Node.js ortamlarını desteklemek istediğinizde ortaya çıkan bir örnek:

 { // ... "main": "./index-cjs.js", "module": "./index-esm.js", "exports": { "require": "./index-cjs.js", "import": "./index-esm.js" } // ... }

Buna ek olarak, Skypack ekibi, belirli bir paketin uzun ömürlülük ve en iyi uygulamalar için ayarlanıp ayarlanmadığını belirlemek için bir ölçüt olarak bir paket kalite puanı getirdi . Araç, GitHub'da açık kaynaklıdır ve her sürümden önce kontrolleri kolayca gerçekleştirmek için paketinize bir devDependency olarak eklenebilir.

Toplama

Umarım bu makale sizin için yararlı olmuştur. Öyleyse, ağınızla paylaşmayı düşünün. Yorumlarda veya Twitter'da sizinle etkileşime geçmek için sabırsızlanıyorum.

Yararlı Kaynaklar

Makaleler ve Belgeler

  • “ES Modülleri: Bir Çizgi Film Derin Dalışı”, Lin Clark, Mozilla Hacks
  • "Ağaç Sallayarak", Web Paketi
  • "Yapılandırma", Web paketi
  • “Optimizasyon”, Web Paketi
  • “Kapsam Kaldırma”, Parsel sürüm 2'nin dokümantasyonu

Projeler ve Araçlar

  • Terser
  • babel-plugin-transform-imports
  • Skypack
  • Web paketi
  • Parsel
  • toplama
  • esbuild
  • SWC
  • Paket Kontrolü