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ğuna ve JavaScript paketlerinizi neden gereğinden büyük hale getirdiğine bakacağız.

Ö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 nedir?

CommonJS, JavaScript modülleri için kurallar oluşturan 2009 tarihli 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, modüllerdeki işlevleri dışa aktarabilir ve modülleri 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 bazılarını veya tümünü içe aktarabilir ve kullanabilir:

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

node ile index.js çağrıldığında konsolda 3 sayısı görüntülenir.

2010'ların başında tarayıcıda standart bir modül sisteminin olmaması nedeniyle 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 lodash iç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 neden neredeyse 16.000 kat daha büyük olmasına neden olur? diye sorabilirsiniz. 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 üzerinde ECMAScript modülleri kullanıyor olsanız bile, kullanmakta olduğunuz modül bir CommonJS modülüyse uygulamanızın paket boyutunun etkileneceğini 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çinde içe aktardığımız bir ECMAScript modülümüz bulunuyor. 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',
};

Elde edilen çıktıyı inceleyelim:

/******/ (() => { // 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 sadeleştirici yukarıdaki kaynak kodunu işlerse, şunları yapar:

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

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'e kıyasla daha statik olarak analiz edilebilir oldukları için ES modülleri için varsayılan olarak etkindir.

Tam olarak aynı örneği inceleyelim ancak bu sefer utils.js'yi ES modülleri yerine CommonJS kullanacak şekilde değiştirelim:

// 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ştım:

...
(() => {

"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 minify aracı, 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 olduklarından analiz edilmesi çok daha zordur. Örneğin, ES modüllerindeki içe aktarma konumu, bir ifadenin bulunduğu 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 eklenti 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ıyla optimize etmesini sağlamak 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.