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

CommonJS modüllerinin uygulamanızın ağaç sallanmasını 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 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.

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ı uygulamalar için tasarlanmıştı.

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

// 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ı kadar önemli değildir. Bu nedenle, CommonJS, üretim paketi boyutunu küçültme göz önünde bulundurularak tasarlanmamıştır. Aynı zamanda, analiz tarayıcı uygulamalarını yavaşlatan bir numaralı nedenin hâlâ JavaScript paket boyutu olduğunu 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 ederken kullanmadığınız kaynak kodundan mümkün olduğunca fazlasını kaldırmaya çalışırlar.

Örneğin, utils.js içinden içe aktardığınız tek simge index.js olduğundan, yukarıdaki snippet'te son paketiniz yalnızca add işlevini içermelidir.

Aşağıdaki webpack yapılandırmasını kullanarak uygulamayı oluşturalım:

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 ve giriş noktası olarak index.js kullanmak istediğimizi belirtiyoruz. webpack çağrıldıktan sonra çıktı boyutunu incelersek şuna benzer bir sonuç görürsünüz:

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

Paketin 625 KB olduğunu unutmayın. Çıkışı incelediğimizde utils.js ürünündeki tüm işlevlerin yanı sıra lodash ürünündeki birçok modülle karşılaşırız. index.js için lodash kullanmasak da bu, çı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 aşağıdaki gibi görünecek:

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

Ayrıca index.js, ECMAScript modülü söz dizimini kullanarak utils.js yolundan içe aktarma yapar:

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

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

Aynı webpack yapılandırmasını kullanarak uygulamamızı geliştirip çıkış dosyasını açabiliriz. Aşağıdaki çıktı ile şu anda 40 bayt:

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

Son paketin, utils.js sağlayıcısından kullanmadığımız işlevlerden hiçbirini içermediğine ve lodash ürününden herhangi bir iz olmadığına dikkat edin. Dahası, terser (webpack işlevinin kullandığı JavaScript küçültücü), add işlevini console.log içinde satır içi yaptı.

CommonJS kullanmak neden çıkış paketinin neredeyse 16.000 kat daha büyük olmasına neden oluyor diye sorabilirsiniz. Elbette bu bir oyuncak örneğidir. Aslında boyut farkı çok büyük olmayabilir ancak CommonJS'nin üretim derlemenize önemli ölçüde ağırlık eklemesi mümkündür.

CommonJS modüllerinin optimizasyonu, ES modüllerinden çok daha dinamik olduğu için genel durumda daha zordur. Paketleyicinizin ve küçültücünüzün 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.

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, uygulamanızı neden daha büyük hale getiriyor?

Bu soruyu yanıtlamak için webpack içindeki ModuleConcatenationPlugin bileşeninin davranışına bakacak ve ardından statik analiz edilebilirliği ele alacağız. Bu eklenti, tüm modüllerinizin kapsamını tek bir kapatmada birleştirir ve kodunuzun tarayıcıda daha hızlı yürütme süresi olmasını 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 oluşturabiliriz ancak bu kez küçültme özelliğini 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 çıkışta, 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:

  • 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 genellikle kullanılmayan içe aktarma işlemlerinin kaldırılmasını ağaç sallama olarak ifade eder. Ağaç sallamasının nedeni web paketinin, utils.js kaynağından hangi sembolleri içe aktardığımızı ve hangi sembolleri dışa aktardığımızı statik olarak (oluşturma sırasında) anlayabildiği için mümkündü.

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

Tam olarak aynı örneğe bakalım, ancak bu kez utils.js özelliğini ES modülleri yerine CommonJS kullanacak şekilde değiştirin:

// 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, sonucu ö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));

})();

Son paketin, bazı webpack "çalışma zamanı" (paketlenmiş modüllerden işlevleri içe/dışa aktarmaktan sorumlu olan yerleştirilmiş kod) içerdiğine dikkat edin. Bu kez, utils.js ve index.js kaynaklı tüm sembolleri aynı ad alanı altına yerleştirmek yerine, çalışma zamanında __webpack_require__ kullanılarak dinamik olarak add işlevi kullanılması gerekir.

Bu gereklidir çünkü CommonJS ile dışa aktarma adını rastgele bir ifadeden alabiliriz. Örneğin, aşağıdaki kod kesinlikle geçerli bir yapıdır:

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

Yalnızca çalışma zamanında mevcut olan bilgileri kullanıcının tarayıcısı bağlamında gerektirdiği için paketleyicinin derleme sırasında dışa aktarılan simgenin adını bilmesi mümkün değildir.

Bu durumda küçültücü, index.js öğesinin bağımlılıklarından tam olarak ne kullandığını anlayamaz ve ağaç sallamasından kurtulamaz. Üçüncü taraf modüllerde de aynı davranışı gözlemleyeceğiz. node_modules ürününden bir CommonJS modülü içe aktarırsak derleme araç zinciriniz bunu düzgün bir şekilde optimize edemez.

CommonJS ile ağaç sallama

Tanımları gereği dinamik oldukları için CommonJS modüllerini analiz etmek çok daha zordur. Örneğin, ES modüllerindeki içe aktarma konumu, bir ifade olduğu CommonJS ile karşılaştırıldığında her zaman bir dize değişmez değeridir.

Bazı durumlarda, kullandığınız kitaplık CommonJS'in kullanımıyla ilgili belirli kurallara uyuyorsa, derleme sırasında kullanılmayan dışa aktarmaları bir üçüncü taraf webpack eklentisi kullanarak kaldırmak mümkündür. Bu eklenti ağaç sallama desteği eklese de bağımlılarınızın CommonJS'i kullanabileceği tüm farklı yolları kapsamaz. Bu durum, ES modülleriyle aynı garantilerden yararlanamayacağınız anlamına gelir. Ayrıca, derleme sürecinizin bir parçası olarak varsayılan webpack davranışının yanı sıra ekstra bir maliyet de 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 iyi yolda olduğunuzu doğrulamak için uygulayabileceğiniz birkaç ipucunu aşağıda bulabilirsiniz:

  • Rollup.js'nin node-resolve yöntemini kullanma eklentisini yükleyin ve yalnızca ECMAScript modüllerine bağlı kalmak istediğinizi belirtmek üzere modulesOnly işaretini ayarlayın.
  • is-esm paketini kullanın .
  • Angular kullanıyorsanız ve ağaçtan sallanmayan modüller kullanıyorsanız varsayılan olarak uyarı alırsınız.