Раньше JavaScript часто записывался в один длинный файл с кодом, разбросанным в неуправляемом беспорядке. Современный JavaScript принес нам модули CommonJS, стандарт, который позволил нам правильно разбить наш код на связанные файлы и легко предоставлять API от одного модуля к другому. Разработчики приветствовали. Кодовые базы обрадовались. Эта конструкция уже была распространена в других языках и отчаянно требовалась в JavaScript. Позже стандарт CommonJS будет заменен поддержкой собственных модулей в EcmaScript 2015 (чаще называемом ES6).

Пример модуля ES6 выглядит следующим образом:

// multiply.js
export default function (num1, num2) {
  return num1 * num2;
}

Это простой модуль, который умножает два числа и возвращает результат. Другой файл, скажем, main.js, может импортировать модуль и использовать его следующим образом:

// main.js
import multiply from './multiply';
const answer = multiply(2, 3);
console.log(answer);

При разработке для браузера сборщики модулей часто используются для объединения десятков или сотен этих модулей в один файл, который может быть доставлен в браузер пользователя. Существует несколько популярных сборщиков модулей, включая Webpack, Rollup, Parcel, Browserify и RequireJS. Упаковщики имеют полное представление о формате модуля. Если мы скажем сборщику связать наш файл main.js, он сможет прочитать содержимое модуля и определить, что код в main.js нуждается в коде multiply.js для правильной работы. В результате сборщик включит в выходной пакет код как из main.js, так и из multiply.js. Более того, сборщик предоставит необходимый связующий код, который будет передавать экспорт модуля умножения (функция multiply) в основной модуль, когда основной модуль его запрашивает. И модули, и код клея включены в выходной комплект.

Традиционные методы комплектации

Традиционно сборщики объединяют каждый модуль в функцию, а затем предоставляют некоторый служебный код, который связывает их вместе. Объединение приведенного выше примера с использованием Webpack, возможно, самого популярного сборщика на данный момент, дает следующий результат при использовании конфигурации по умолчанию:

/******/ (function(modules) { // webpackBootstrap
/******/   // The module cache
/******/   var installedModules = {};
/******/
/******/   // The require function
/******/   function __webpack_require__(moduleId) {
/******/
/******/      // Check if module is in cache
/******/      if(installedModules[moduleId]) {
/******/         return installedModules[moduleId].exports;
/******/      }
/******/      // Create a new module (and put it into the cache)
/******/      var module = installedModules[moduleId] = {
/******/         i: moduleId,
/******/         l: false,
/******/         exports: {}
/******/      };
/******/
/******/      // Execute the module function
/******/      modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/      // Flag the module as loaded
/******/      module.l = true;
/******/
/******/      // Return the exports of the module
/******/      return module.exports;
/******/   }
/******/
/******/
/******/   // expose the modules object (__webpack_modules__)
/******/   __webpack_require__.m = modules;
/******/
/******/   // expose the module cache
/******/   __webpack_require__.c = installedModules;
/******/
/******/   // define getter function for harmony exports
/******/   __webpack_require__.d = function(exports, name, getter) {
/******/      if(!__webpack_require__.o(exports, name)) {
/******/         Object.defineProperty(exports, name, {
/******/            configurable: false,
/******/            enumerable: true,
/******/            get: getter
/******/         });
/******/      }
/******/   };
/******/
/******/   // getDefaultExport function for compatibility with non-harmony modules
/******/   __webpack_require__.n = function(module) {
/******/      var getter = module && module.__esModule ?
/******/         function getDefault() { return module['default']; } :
/******/         function getModuleExports() { return module; };
/******/      __webpack_require__.d(getter, 'a', getter);
/******/      return getter;
/******/   };
/******/
/******/   // Object.prototype.hasOwnProperty.call
/******/   __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/   // __webpack_public_path__
/******/   __webpack_require__.p = "";
/******/
/******/   // Load entry module and return exports
/******/   return __webpack_require__(__webpack_require__.s = 0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__multiply__ = __webpack_require__(1);
const answer = Object(__WEBPACK_IMPORTED_MODULE_0__multiply__["a" /* default */])(2, 3);
console.log(answer);
/***/ }),
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
/* harmony default export */ __webpack_exports__["a"] = (function (num1, num2) {
  return num1 * num2;
});
/***/ })
/******/ ]);

Я выделил наш исходный код, хотя и несколько искаженный, жирным шрифтом. Как видите, добавлено довольно много служебного кода. Когда мы отправляем код в браузер пользователя, мы обычно минимизируем код, что в данном случае устраняет довольно много лишнего:

!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{configurable:!1,enumerable:!0,get:r})},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(1);const o=Object(r.a)(2,3);console.log(o)},function(e,t,n){"use strict";t.a=function(e,t){return e*t}}]);

Опять же, я выделил наш исходный (еще более искаженный) код жирным шрифтом. Это все еще довольно много накладных расходов. Некоторые из этих накладных расходов повторяются для каждого модуля, который у нас есть. Как мы увидим, это может существенно повлиять на размер и производительность.

Подъем прицела

За последние несколько лет бандлер под названием Rollup приобрел значительную популярность, и не зря. В Rollup совершенно иной подход к объединению модулей. Вместо того, чтобы заключать каждый модуль в функцию и добавлять код для связывания функций вместе, он поднимает весь код модуля в одну функцию. Это требует особой осторожности, чтобы гарантировать, что исходное поведение сохраняется, а также предотвращает конфликты имен переменных, среди прочего. Объединение приведенного выше примера с помощью Rollup дает следующий результат:

(function () {
'use strict';
function multiply (num1, num2) {
  return num1 * num2;
}
const answer = multiply(2, 3);
console.log(answer);
}());

Обратите внимание, как содержимое обоих модулей было объединено (поднято) в область единой функции. Отсутствие накладных расходов при сохранении запланированного поведения. А теперь минифицировано:

!function(){"use strict";const o=2*3;console.log(o)}();

В этом случае минификатор был достаточно умен (и подъем области действия сделал его достаточно простым), чтобы полностью исключить функцию умножения. Двойной выигрыш.

Турбина: пример из реальной жизни

Для Запуск платформы Adobe Experience у нас есть что-то вроде механизма правил, который доставляется на веб-сайты наших клиентов, в конечном итоге загружается и запускается миллионами раз пользователями по всему миру. Мы называем это правило двигателем Turbine. Это открытый исходный код, и вы можете найти код на GitHub. Наши клиенты полагаются на нас, чтобы оптимизировать наш код там, где это возможно, чтобы обеспечить удобство для своих пользователей.

По состоянию на октябрь 2017 года мы использовали Webpack для сборки Turbine. Это работало нормально, но подъем выглядел многообещающим способом уменьшить вес кода, поэтому мы решили попробовать Rollup. На размер файла повлияло следующее:

Использование Webpack без поднятия области видимости:

  • Оригинал: 123,0 КБ
  • Минимизировано: 32,0 КБ
  • Gzip: 10,8 КБ

Использование Rollup с подъемом области:

  • Оригинал: 75,1 КБ
  • Минимизировано: 17,2 КБ
  • Gzip: 6,4 КБ

Кроме того, мы измерили время, необходимое браузеру для выполнения первоначального запуска Turbine после его загрузки. При сравнении с Chrome на высокопроизводительном MacBook Pro 2017 года среднее время, затрачиваемое на 10 запусков, было следующим:

  • Использование Webpack без поднятия области видимости: 9,61 миллисекунды
  • Использование Rollup с поднятием области видимости: 8,43 миллисекунды

Излишне говорить, что мы остались очень довольны результатом. Приложив немного усилий с нашей стороны, мы смогли уменьшить размер Turbine в сжатом виде на ~ 41% и сократить время начального выполнения на ~ 12%.

Подъемная опора

Накопительный пакет, возможно, был первым, кто поддержал подъем области видимости, но вскоре его примеру последовали и другие сборщики пакетов. Webpack добавил поддержку подъема области видимости в Webpack 3 с помощью ModuleConcatenationPlugin. Подъем может быть реализован в Browserify с помощью Rollupify и доступен в Parcel с использованием экспериментального флага.

Хотя в итоге мы выбрали Rollup для объединения Turbine (нам не потребовались какие-либо функции, которых не хватало Rollup), будьте уверены, что у вас есть варианты подъема области с другими пакетами. Поместите свой код в диету по подъему осциллографа и дайте мне знать, как это происходит.