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 fazla büyüttüğüne değineceğiz.

Özet: Paketleyicinin uygulamanızı başarılı bir şekilde optimize edebilmesi için, CommonJS modüllerine bağlı kalmaktan 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 belirleyen, 2009 yılından kalma bir standarttır. Başlangıçta web tarayıcısı dışında, özellikle sunucu tarafı uygulamalar için tasarlanmıştı.

CommonJS ile modül tanımlayabilir, bu modüllerden işlevleri dışa aktarabilir ve diğer modüllere içe 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 aşağıdaki 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ı çıktı.

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

CommonJS nihai grup boyutunuzu nasıl etkiler?

Sunucu tarafı JavaScript uygulamanızın boyutu tarayıcıdaki kadar önemli değildir. Bu nedenle CommonJS, üretim paketi boyutu küçültülerek tasarlanmamıştır. Aynı zamanda, analiz tarayıcı uygulamalarını yavaşlatmanın bir numaralı nedenin hâlâ JavaScript paketi boyutu olduğunu gösteriyor.

webpack ve terser gibi JavaScript paketleyicileri 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 koddan mümkün olduğunca çok sayıda içeriği kaldırmaya çalışırlar.

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

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

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

Paketin 625 KB olduğuna dikkat edin. Sonuca baktığımızda, utils.js kapsamındaki tüm işlevlerin yanı sıra lodashprojesindeki birçok modülle karşılaşırız. index.js ürününde lodash kullanmasak da bu, çıktının bir parçasıdır. Bu da üretim öğelerimize çok fazla ağırlık katar.

Şimdi modül biçimini ECMAScript modülleri olarak değiştirip tekrar deneyelim. Bu sefer utils.js şu şekilde 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);

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

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

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

Aynı webpack yapılandırmasını kullanarak uygulamamızı derleyip çıkış dosyasını açabiliriz. Şu anda 40 bayt ve şu çıkış:

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

Son paketin, utils.js ile ilgili kullanmadığımız işlevlerin hiçbirini içermediğini ve lodash ürününden herhangi bir iz kalmadığına dikkat edin. Daha da ayrıntılı olarak, terser (webpack tarafından kullanılan JavaScript küçültücü), console.log içindeki add işlevini satır içine aldı.

CommonJS kullanmak neden çıktı paketinin yaklaşık 16.000 kat daha büyük olmasına neden olur, makul bir soru sorabilirsiniz. Elbette bu bir oyuncak örneğidir. Gerçekte, boyut farkı o kadar büyük olmayabilir, ancak CommonJS'nin üretim derlemenize önemli bir ağırlık katma olasılığı yüksektir.

ES modüllerinden çok daha dinamik oldukları için CommonJS modüllerinin genel olarak optimize edilmesi daha zordur. Paketleyici ve küçültücünüzün uygulamanızı başarılı bir şekilde optimize edebilmesi için, CommonJS modüllerini kullanmaktan kaçının ve uygulamanızın tamamında ECMAScript modülü söz dizimini kullanın.

index.js ürününde ECMAScript modülleri kullanıyorsanız bile, kullandığınız 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 davranışını inceleyeceğiz ve ardından, statik analiz edilebilirliği ele alacağız. Bu eklenti, tüm modüllerinizin kapsamını tek bir kapanışta birleştirir. Böylece kodunuzun tarayıcıda daha hızlı yürütülmesini sağlar. Bir örneğe bakalım:

// 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 var. 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üçültmeyi 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',
};

Üretilen çıkışı 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ı içindedir. Webpack, çakışmaları önlemek için index.js içindeki subtract işlevini index_subtract olarak yeniden adlandırdı.

Bir küçültücü yukarıdaki kaynak kodu 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çine alın

Geliştiriciler genellikle bu kullanılmayan içe aktarma işlemlerinin kaldırılmasını ağaç sallama olarak görür. Ağaç sallama, yalnızca webpack utils.js öğesinden hangi simgeleri içe aktardığımızı (derleme sırasında) statik olarak anlayabildiği için mümkündü.

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

Tam olarak aynı örneğe bakalım, ancak bu sefer utils.js değişikliğini ES modülleri yerine CommonJS'yi 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 gömmek için çok 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 pakette webpack "runtime" (çalışma zamanı) (eklenmiş modüllerdeki işlevlerinin içe/dışa aktarılmasından sorumlu, yerleştirilmiş kod) bulunduğuna dikkat edin. Bu kez, utils.js ve index.js içindeki tüm simgeleri aynı ad alanı altına yerleştirmek yerine, çalışma zamanında dinamik olarak __webpack_require__ kullanarak add işlevini gerekli kılıyoruz.

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

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

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

Böylece, küçültücü index.js ürününün bağımlılıklarından tam olarak ne kullandığını anlayamaz ve bu yüzden onu ağaçtan sallayamaz. Tam olarak aynı davranışı üçüncü taraf modüllerde de izleyeceğiz. node_modules kaynağından bir CommonJS modülü içe aktarırsak derleme araç zinciriniz bunu düzgün şekilde optimize edemez.

CommonJS ile ağaç sallama

CommonJS modülleri tanım gereği dinamik olduğundan bu modülleri analiz etmek çok daha zordur. Örneğin, ES modüllerindeki içe aktarma konumu, bir ifade olduğu CommonJS'ye kıyasla her zaman dize değişmez değeridir.

Bazı durumlarda, kullandığınız kitaplık CommonJS'nin kullanımına dair belirli kurallara uyuyorsa derleme zamanında üçüncü taraf bir webpack plugin kullanarak kullanılmayan dışa aktarmaları kaldırmak mümkündür. Bu eklenti ağaç sallama desteği eklese de, bağımlılıklarınızın CommonJS'yi kullanabileceği farklı yolları kapsamaz. Bu durumda, ES modülleriyle aynı garantileri almazsınız. Ayrıca, varsayılan webpack davranışının üzerine derleme işleminiz kapsamında ekstra bir maliyet getirir.

Sonuç

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

Aşağıda, optimum yolda olduğunuzu doğrulamanızı sağlayacak birkaç işlem yapılabilir ipucu verilmiştir:

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