CommonJS paketlerinizi nasıl büyütüyor?

CommonJS modüllerinin uygulamanızın ağaç sallama işlemini nasıl etkilediğini öğrenin

Bu yayında, CommonJS'nin ne olduğunu ve JavaScript paketlerinizi neden gereğinden fazla büyüttüğünü inceleyeceğiz.

Özet: Paketleyicinin uygulamanızı başarılı bir şekilde optimize edebilmesi için CommonJS modüllerine bağımlı olmaktan kaçının ve uygulamanızın tamamında ECMAScript modülü söz dizimini kullanın.

CommonJS, 2009'dan beri JavaScript modülleri için kurallar belirleyen bir standarttır. Başlangıçta web tarayıcısının dışında, özellikle sunucu tarafı uygulamalarda kullanılmak üzere tasarlanmıştır.

CommonJS ile modüller tanımlayabilir, bu modüllerdeki işlevleri dışa aktarabilir ve diğer modüllere aktarabilirsiniz. Örneğin, aşağıdaki snippet'te add, subtract, multiply, divide ve max olmak üzere beş işlevi dışa aktaran bir modül tanımlanmaktadır:

// utils.js
const { maxBy } = require('lodash-es');
const fns = {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b,
  multiply: (a, b) => a * b,
  divide: (a, b) => a / b,
  max: arr => maxBy(arr)
};

Object.keys(fns).forEach(fnName => module.exports[fnName] = fns[fnName]);

Daha sonra başka bir modül bu işlevlerin bir kısmını veya tamamını içe aktarıp kullanabilir:

// index.js
const { add } = require('./utils.js');
console.log(add(1, 2));

index.js işlevi node ile çağrılırsa konsolda 3 numarası gösterilir.

2010'ların başlarında tarayıcıda standartlaştırılmış bir modül sistemi olmadığından CommonJS, JavaScript istemci tarafı kitaplıkları için de popüler bir modül biçimi haline geldi.

CommonJS nihai paket boyutunuzu nasıl etkiler?

Sunucu tarafı JavaScript uygulamanızın boyutu, tarayıcıdaki kadar kritik değildir. Bu nedenle CommonJS, üretim paketi boyutunu azaltma amacıyla tasarlanmamıştır. Aynı zamanda analizler, JavaScript paketi boyutunun tarayıcı uygulamalarını yavaşlatan birincil neden olmaya devam ettiğini gösteriyor.

webpack ve terser gibi JavaScript paketleyiciler ve küçültücüler, uygulamanızın boyutunu küçültmek için farklı optimizasyonlar gerçekleştirir. Derleme sırasında uygulamanızı analiz ederek kullanmadığınız kaynak kodundan mümkün olduğunca fazlasını kaldırmaya çalışırlar.

Örneğin, yukarıdaki snippet'te nihai paketiniz yalnızca add işlevini içermelidir. Çünkü utils.js'den index.js'ye içe aktardığınız tek simge budur.

Uygulamayı aşağıdaki webpack yapılandırmasını kullanarak derleyelim:

const path = require('path');
module.exports = {
  entry: 'index.js',
  output: {
    filename: 'out.js',
    path: path.resolve(__dirname, 'dist'),
  },
  mode: 'production',
};

Burada, üretim modu optimizasyonlarını kullanmak istediğimizi ve giriş noktası olarak index.js değerini kullanacağımızı belirtiyoruz. webpack işlevini çağırdıktan sonra çıktı boyutunu keşfedersek aşağıdaki gibi bir şey görürüz:

$ cd dist && ls -lah
625K Apr 13 13:04 out.js

Paketin 625 KB olduğunu unutmayın. Çıktıya bakarsak utils.js içindeki tüm işlevlerin yanı sıra lodashiçindeki birçok modülü görürüz. lodash, index.js'te kullanılmamasına rağmen çıktının bir parçasıdır. Bu da üretim öğelerimize çok fazla ağırlık ekler.

Şimdi modül biçimini ECMAScript modülleri olarak değiştirip tekrar deneyelim. Bu sefer utils.js şöyle görünür:

export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export const multiply = (a, b) => a * b;
export const divide = (a, b) => a / b;

import { maxBy } from 'lodash-es';

export const max = arr => maxBy(arr);

index.js ise ECMAScript modülü söz dizimini kullanarak utils.js'ten içe aktarır:

import { add } from './utils.js';

console.log(add(1, 2));

Aynı webpack yapılandırmasını kullanarak uygulamamızı derleyebilir ve çıkış dosyasını açabiliriz. Artık çıktı aşağıdaki gibi 40 bayt:

(()=>{"use strict";console.log(1+2)})();

Nihai pakette, utils.js içindeki kullanmadığımız işlevlerin hiçbirinin bulunmadığını ve lodash'dan hiçbir iz olmadığını fark edin. Daha da ileri giderek terser (webpack'un kullandığı JavaScript sadeleştirici), add işlevini console.log içine yerleştirdi.

CommonJS'in kullanılması, çıkış paketinin neredeyse 16.000 kat daha büyük olmasına neden oluyor. Elbette bu örnek küçüktür. Gerçekte boyut farkı bu kadar büyük olmayabilir ancak CommonJS'in üretim derlemenize önemli ölçüde ağırlık katması olasıdır.

CommonJS modülleri, ES modüllerinden çok daha dinamik oldukları için genel olarak optimize edilmesi daha zordur. Paketleyicinizin ve sıkıştırıcınızın uygulamanızı başarılı bir şekilde optimize edebilmesi için CommonJS modüllerine bağımlı olmaktan kaçının ve uygulamanızın tamamında ECMAScript modülü söz dizimini kullanın.

index.js içinde ECMAScript modülleri kullanıyor olsanız bile, kullandığınız modül bir CommonJS modülüyse uygulamanızın paket boyutunun artacağını unutmayın.

CommonJS neden uygulamanızı büyütür?

Bu soruyu yanıtlamak için webpack içindeki ModuleConcatenationPlugin öğesinin davranışına bakacağız ve ardından statik analiz edilebilirlik hakkında konuşacağız. Bu eklenti, tüm modüllerinizin kapsamını tek bir kapatma içine alır ve kodunuzun tarayıcıda daha hızlı yürütülmesini sağlar. Bunu bir örnek üzerinde inceleyelim:

// utils.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
// index.js
import { add } from './utils.js';
const subtract = (a, b) => a - b;

console.log(add(1, 2));

Yukarıda, index.js içine içe aktardığımız bir ECMAScript modülü var. Ayrıca bir subtract işlevi de tanımlarız. Projeyi yukarıdakiyle aynı webpack yapılandırmasını kullanarak derleyebiliriz ancak bu kez sadeleştirmeyi devre dışı bırakacağız:

const path = require('path');

module.exports = {
  entry: 'index.js',
  output: {
    filename: 'out.js',
    path: path.resolve(__dirname, 'dist'),
  },
  optimization: {
    minimize: false
  },
  mode: 'production',
};

Oluşturulan çıktıya bakalım:

/******/ (() => { // webpackBootstrap
/******/    "use strict";

// CONCATENATED MODULE: ./utils.js**
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;

// CONCATENATED MODULE: ./index.js**
const index_subtract = (a, b) => a - b;**
console.log(add(1, 2));**

/******/ })();

Yukarıdaki çıktıda tüm işlevler aynı ad alanındadır. Çakışmaları önlemek için webpack, index.js içindeki subtract işlevini index_subtract olarak yeniden adlandırdı.

Bir kod sıkıştırıcı yukarıdaki kaynak kodunu işlerse:

  • Kullanılmayan subtract ve index_subtract işlevlerini kaldırın
  • Tüm yorumları ve gereksiz boşlukları kaldırın
  • console.log çağrısında add işlevinin gövdesini satır içi olarak yerleştirme

Geliştiriciler, kullanılmayan içe aktarma işlemlerinin kaldırılmasını genellikle ağaç sallama olarak adlandırır. Ağ sallama işleminin yapılabilmesi için webpack'in utils.js'ten hangi sembolleri içe aktardığımızı ve hangi sembolleri dışa aktardığını statik olarak (derleme sırasında) anlayabilmesi gerekir.

Bu davranış, CommonJS'ye kıyasla daha statik olarak analiz edilebilir oldukları için ES modülleri için varsayılan olarak etkindir.

Aynı örneği inceleyelim ancak bu sefer utils.js için ES modülleri yerine CommonJS kullanalım:

// utils.js
const { maxBy } = require('lodash-es');

const fns = {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b,
  multiply: (a, b) => a * b,
  divide: (a, b) => a / b,
  max: arr => maxBy(arr)
};

Object.keys(fns).forEach(fnName => module.exports[fnName] = fns[fnName]);

Bu küçük güncelleme, çıkışı önemli ölçüde değiştirecektir. Bu sayfaya yerleştirilemeyecek kadar uzun olduğundan yalnızca küçük bir kısmını paylaşıyorum:

...
(() => {

"use strict";
/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(288);
const subtract = (a, b) => a - b;
console.log((0,_utils__WEBPACK_IMPORTED_MODULE_0__/* .add */ .IH)(1, 2));

})();

Nihai paketin bazı webpack "çalışma zamanı" içerdiğini unutmayın: Paketlenmiş modüllerdeki işlevleri içe/dışa aktarmaktan sorumlu olan eklenmiş kod. Bu sefer, utils.js ve index.js'daki tüm sembolleri aynı ad alanının altına yerleştirmek yerine, çalışma zamanında dinamik olarak __webpack_require__ kullanarak add işlevini kullanmamız gerekir.

CommonJS ile dışa aktarma adını rastgele bir ifadeden alabileceğimiz için bu gereklidir. Örneğin, aşağıdaki kod kesinlikle geçerli bir yapıdır:

module.exports[localStorage.getItem(Math.random())] = () => {  };

Bu işlem için yalnızca çalışma zamanında, kullanıcının tarayıcısı bağlamında kullanılabilen bilgiler gerektiğinden, paketleyicinin derleme sırasında dışa aktarılan sembolün adını bilmesi mümkün değildir.

Bu sayede, index.js'ın bağımlılıklarından tam olarak neleri kullandığını anlayamayan sıkıştırıcı, bu bağımlılıklardan kurtulamaz. Üçüncü taraf modülleri için de tam olarak aynı davranışı gözlemleyeceğiz. node_modules adresinden bir CommonJS modülü içe aktarırsak derleme araç zinciriniz bu modülü düzgün şekilde optimize edemez.

CommonJS ile ağaç sallama

CommonJS modülleri, tanımları gereği dinamik olduğundan analiz edilmesi çok daha zordur. Örneğin, ES modüllerindeki içe aktarma konumu, bir ifadenin yer aldığı CommonJS'ye kıyasla her zaman bir dize değişmez.

Bazı durumlarda, kullandığınız kitaplık CommonJS'yi kullanma konusunda belirli kurallara uyuyorsa kullanılmayan dışa aktarma işlemlerini derleme sırasında üçüncü taraf bir webpack plugin kullanarak kaldırabilirsiniz. Bu eklenti, ağaç sallama desteği eklese de bağımlılıklarınızın CommonJS'i kullanabileceği tüm farklı yolları kapsamaz. Bu, ES modülleriyle aynı garantileri almayacağınız anlamına gelir. Ayrıca, varsayılan webpack davranışına ek olarak derleme sürecinizin bir parçası olarak ek maliyet ekler.

Sonuç

Paketleyicinin uygulamanızı başarılı bir şekilde optimize edebilmesi için CommonJS modüllerine bağımlı olmaktan kaçının ve uygulamanızın tamamında ECMAScript modülü söz dizimini kullanın.

En uygun yolda olduğunuzu doğrulamak için uygulayabileceğiniz birkaç ipucu aşağıda verilmiştir:

  • Rollup.js'nin node-resolve eklentisini kullanın ve yalnızca ECMAScript modüllerine bağımlı olmak istediğinizi belirtmek için modulesOnly işaretini ayarlayın.
  • Bir npm paketinin ECMAScript modülleri kullandığını doğrulamak için is-esm paketini kullanın.
  • Angular kullanıyorsanız ağaç sallanamayabilecek modüllere bağımlıysanız varsayılan olarak uyarı alırsınız.