Вивчай
· 14 хв читання
javascriptвеб-розробкапочатківцям

Історія JavaScript-модулів — від глобального хаосу до ESM

Еволюція JavaScript-модулів — від хаотичних script-тегів до сучасних import/exportЕволюція JavaScript-модулів — від хаотичних script-тегів до сучасних import/export

Уяви: ти підключаєш 15 скриптів через <script> до HTML-сторінки. Кожен може перезаписати змінні іншого. Все працює — поки хтось не додасть 16-й скрипт, і сайт розсипається. Ти дивишся в консоль, а там: undefined is not a function. Знайомо? Це був JavaScript до модулів.

За 30 років мова пройшла шлях від повного хаосу до елегантного import/export. Ця стаття — хронологія того шляху: хто, коли і навіщо створював кожне рішення. З кодом, фактами та іменами.

Якщо тебе цікавить саме синтаксис import/export — подивись наш урок про ES6 модулі. А тут ми розберемо чому модулі з'явилися і який шлях пройшли.


Ера 1: Дикий Захід — глобальний хаос (1995–2005)

У травні 1995 року Brendan Eich створив JavaScript за 10 днів у компанії Netscape. Мова призначалася для маленьких скриптів: підсвітити кнопку, валідувати форму, показати alert. Ніхто не уявляв застосунків з тисячами рядків коду.

Тому модульної системи не було. Взагалі. Весь код жив у одному глобальному просторі — об'єкті window. А сайти росли: головна сторінка Yahoo у 2003 підключала десятки скриптів, Gmail (2004) став одним з перших "веб-застосунків" з тисячами рядків JavaScript. Код ставав складнішим — а інструментів для організації не було:

// counter.js
var count = 0;
function increment() {
  count++;
}

// analytics.js (інший розробник, інший файл)
var count = "pageviews"; // 💥 перезаписали чужу змінну!

// counter.js тепер зламаний — increment() додає 1 до рядка "pageviews"

Чим більше файлів — тим більше конфліктів. Розробники називали це global namespace pollution (забруднення глобального простору). І почали шукати вихід.

Namespace pattern — перша спроба порядку

У 2002 році бібліотека Bindows (автор — Erik Arvidsson) запропонувала елегантний хак: замість глобальних функцій — зберігати все як властивості одного об'єкта:

// Namespace pattern — 2002
var MyApp = MyApp || {};

MyApp.utils = {
  formatDate: function(date) {
    return date.toLocaleDateString('uk-UA');
  },
  parseUrl: function(url) {
    return new URL(url);
  }
};

// Інший файл може безпечно додати свій namespace
MyApp.analytics = {
  track: function(event) { /* ... */ }
};

Бібліотеки Dojo (2005) та YUI від Yahoo (2005) масово використовували цей підхід. Він зменшив хаос — але не усунув його повністю. Один об'єкт MyApp все ще був глобальним. І приватних змінних — жодних.


Ера 2: IIFE — перше справжнє рішення (2003–2010)

Розробники помітили цікаву властивість JavaScript: функція створює нову область видимості. Змінні всередині функції не потрапляють у глобальний простір. А якщо цю функцію одразу викликати — отримуємо ізольований блок коду:

// IIFE — Immediately Invoked Function Expression
(function() {
  var secret = "цю змінну ніхто не бачить ззовні";
  console.log(secret); // працює
})();

console.log(secret); // ReferenceError: secret is not defined

Паттерн існував з початку 2000-х, але офіційну назву IIFE дав Ben Alman у своєму блозі в листопаді 2010 року. До цього розробники називали його "self-executing anonymous function" — що технічно неточно, бо функція не "сама себе виконує", а викликається одразу після оголошення. Alman детально пояснив різницю і закріпив термін, яким ми користуємось і сьогодні.

Revealing Module Pattern — повноцінні модулі без модулів

IIFE швидко еволюціонувала в потужний паттерн — Revealing Module Pattern. Ідея: створити приватний простір через IIFE, а назовні повернути тільки публічний API:

// Revealing Module Pattern — модуль через IIFE
var Counter = (function() {
  // приватна змінна — недоступна ззовні
  var count = 0;

  // приватна функція
  function log(action) {
    console.log(action + ': ' + count);
  }

  // повертаємо тільки публічний API
  return {
    increment: function() { count++; log('increment'); },
    decrement: function() { count--; log('decrement'); },
    getCount: function() { return count; }
  };
})();

Counter.increment(); // increment: 1
Counter.increment(); // increment: 2
console.log(Counter.getCount()); // 2
console.log(Counter.count);      // undefined — приватне!

Це був прорив. Вперше JavaScript мав щось схоже на модулі: інкапсуляція (приватні дані), публічний API (що видно ззовні), ізоляція (немає конфліктів).

jQuery (2006) та тисячі його плагінів використовували саме цей паттерн:

// Типовий jQuery-плагін — IIFE з параметром
(function($) {
  $.fn.highlight = function(color) {
    return this.css('background-color', color || 'yellow');
  };
})(jQuery);

IIFE вирішував проблему ізоляції, але залишав відкритим питання залежностей: як один модуль підключає інший? Як контролювати порядок завантаження? Для цього потрібна була система.


Ера 3: CommonJS — модулі виходять за межі браузера (2009)

29 січня 2009 року інженер Mozilla Kevin Dangoor опублікував пост "What Server Side JavaScript Needs" і створив проект ServerJS. Ідея: JavaScript потрібен стандартний спосіб організації коду для серверних застосунків. У серпні 2009-го проект перейменували в CommonJS — щоб підкреслити універсальність.

У тому ж травні 2009-го Ryan Dahl представив Node.js на JSConf EU і обрав CommonJS як свою модульну систему. Ця 45-хвилинна доповідь змінила індустрію — подивись, якщо ще не бачив. Рішення взяти CommonJS визначило долю JavaScript на серверах на наступне десятиліття.

Синтаксис був простим і зрозумілим:

// math.js — експорт через module.exports
function add(a, b) {
  return a + b;
}

function multiply(a, b) {
  return a * b;
}

module.exports = { add, multiply };
// app.js — імпорт через require()
const { add, multiply } = require('./math');

console.log(add(2, 3));      // 5
console.log(multiply(4, 5)); // 20

Нарешті: явні залежності, приватний простір за замовчуванням, зрозумілий синтаксис. Але був критичний нюанс.

Синхронний require() — благо і прокляття

require() працює синхронно — він блокує виконання, поки файл не буде прочитаний та виконаний. На сервері це не проблема: файли читаються з диска за мікросекунди. Але в браузері?

// На сервері (Node.js) — миттєво
const utils = require('./utils'); // читає з диска — ~0.1мс

// У браузері це означало б:
const utils = require('https://cdn.example.com/utils.js');
// Завантаження по мережі: 50-500мс
// Весь JavaScript зависає, поки файл не завантажиться!

Синхронне завантаження модулів по HTTP — це заморозка інтерфейсу. Ця проблема безпосередньо привела до появи альтернативного рішення для браузерів.

Порада

Якщо ти бачиш require() і module.exports у коді — це CommonJS. У 2026 році він все ще зустрічається в Node.js проєктах, хоча активно замінюється на ES Modules.


Ера 4: AMD та RequireJS — асинхронність для браузера (2009–2011)

Паралельно з CommonJS James Burke (розробник з команди Dojo Toolkit у Mozilla) працював над іншим підходом. У вересні 2009 він почав створювати RequireJS — інструмент для асинхронного завантаження модулів у браузері.

У 2010 році Kris Zyp (теж з команди Dojo) формалізував специфікацію AMD — Asynchronous Module Definition. Ключова ідея: модулі декларують свої залежності, а завантажувач підвантажує їх паралельно, без блокування.

// AMD синтаксис — модулі завантажуються асинхронно
define('cart', ['jquery', './products', './pricing'], function($, products, pricing) {
  // jquery, products і pricing вже завантажені — можна працювати

  function addToCart(productId) {
    var product = products.getById(productId);
    var price = pricing.calculate(product);
    $('#cart-total').text(price + ' грн');
  }

  // Повертаємо публічний API модуля
  return { addToCart: addToCart };
});
// Використання модуля
require(['cart'], function(cart) {
  cart.addToCart(42);
});

Два стандарти — одна проблема

Тепер JavaScript мав два несумісних стандарти модулів:

CommonJSAMD
ЗавантаженняСинхроннеАсинхронне
Де працюєСервер (Node.js)Браузер
Синтаксисrequire() / module.exportsdefine() / require()
ЗалежностіЗавантажуються при викликуДекларуються заздалегідь

Для авторів бібліотек це був кошмар: писати код двічі — для Node.js і для браузера? Спільнота потребувала компромісу.


Ера 5: UMD — потворний, але робочий компроміс

UMD (Universal Module Definition) з'явився на початку 2010-х як прагматичне рішення: одна обгортка, яка працює всюди — у CommonJS (Node.js), в AMD (RequireJS), і навіть як глобальна змінна у звичайному <script>:

// UMD — Universal Module Definition
(function(root, factory) {
  if (typeof define === 'function' && define.amd) {
    define(['dependency'], factory);                    // AMD
  } else if (typeof module === 'object' && module.exports) {
    module.exports = factory(require('dependency'));     // CommonJS
  } else {
    root.MyLibrary = factory(root.Dependency);          // Глобальна змінна
  }
})(typeof self !== 'undefined' ? self : this, function(dependency) {
  // Тут живе код бібліотеки
  return {
    doSomething: function() { /* ... */ }
  };
});

Подивись на цей код. Серйозно. 15 рядків обгортки — і жоден з них не стосується логіки самої бібліотеки. jQuery, Lodash, Underscore.js, Moment.js — всі популярні бібліотеки того часу використовували UMD для дистрибуції.

Це працювало. Але всім було зрозуміло: JavaScript потребує нормальний стандарт.


Ера 6: Бандлери — Browserify та Webpack (2011–2014)

Поки спільнота сперечалась про AMD vs CommonJS, з'явився радикально інший підхід: а що, якщо не завантажувати модулі по одному, а зібрати все в один файл заздалегідь?

Browserify (2011)

James Halliday (відомий як substack) створив Browserify — інструмент, який аналізує require() виклики у твоєму коді, знаходить усі залежності, і збирає все в один файл, зрозумілий браузеру. Тепер можна було писати CommonJS-код і запускати його в браузері:

// Було: 15 тегів у правильному порядку (один переставиш — все зламається)
<script src="jquery.js"></script>
<script src="underscore.js"></script>
<script src="backbone.js"></script>
<script src="utils.js"></script>
<script src="models.js"></script>
<script src="views.js"></script>
<script src="app.js"></script>

// Стало: один файл
<script src="bundle.js"></script>

Webpack (2014)

У лютому 2014 року Tobias Koppers випустив Webpack — і змінив все. Цікаво, що Webpack виріс з його магістерської дисертації — Koppers шукав спосіб ефективно розбивати код на частини для завантаження за потребою. Webpack не просто збирав JavaScript — він розглядав будь-який файл як модуль: CSS, зображення, шрифти, JSON. А ще додав code splitting, loaders та plugin-систему.

Webpack став індустріальним стандартом на наступні 7 років. Але його конфігурація... стала мемом:

// webpack.config.js — типовий конфіг, від якого боліла голова
module.exports = {
  entry: './src/index.js',
  output: { filename: 'bundle.js', path: __dirname + '/dist' },
  module: {
    rules: [
      { test: /\.js$/, use: 'babel-loader', exclude: /node_modules/ },
      { test: /\.css$/, use: ['style-loader', 'css-loader'] },
      { test: /\.(png|jpg)$/, use: 'file-loader' }
    ]
  },
  plugins: [ /* ще 20 рядків... */ ]
};

Популярний жарт того часу: "Я витратив 3 години на конфігурацію Webpack і 30 хвилин на написання коду".

Rollup і tree shaking (2015)

У 2015 році Rich Harris (автор Svelte) створив Rollup — бандлер, побудований навколо ES Modules. Саме Rollup популяризував концепцію tree shakingвидалення невикористаного коду з фінального бандла. Термін прижився: якщо ти імпортуєш одну функцію з величезної бібліотеки — tree shaking "струшує" все зайве, залишаючи тільки те, що реально використовується.

Rollup виявився надзвичайно ефективним для бібліотек — і пізніше став основою для Vite (production builds). Але попри різноманіття інструментів, бандлери вирішили реальну проблему і створили сучасний pipeline розробки, яким ми користуємось і сьогодні.


Ера 7: ES Modules — стандарт нарешті прийшов (2015)

Наприкінці липня 2014 року комітет TC39 (група, яка розвиває JavaScript) фіналізував синтаксис модулів для ECMAScript 6. У червні 2015 стандарт був офіційно опублікований як ES2015.

Вперше за 20 років JavaScript отримав вбудовану, офіційну модульну систему. Не паттерн. Не бібліотеку. Не хак. Частину самої мови.

// ES Modules — чистий, офіційний стандарт
import { add, multiply } from './math.js';

export function calculate(a, b) {
  return add(a, b) + multiply(a, b);
}

// Порівняй з UMD-монстром вище. Відчуваєш різницю?

Детальний синтаксис import/export з усіма варіантами ми розбираємо в уроці про ES6 модулі. А тут — чому це стало переломним моментом.

Чому ES Modules — це не просто новий синтаксис

Статичний аналіз. На відміну від CommonJS, де require() можна викликати де завгодно (в if, в циклі, з динамічним шляхом), import завжди на верхньому рівні файлу. Це дозволяє інструментам аналізувати залежності без виконання коду.

Tree shaking. Пам'ятаєш Rollup з попередньої ери? Саме статична структура ESM зробила tree shaking можливим. Бандлер може визначити, який код реально використовується, і видалити все зайве з фінального файлу. Імпортуєш одну функцію з бібліотеки на 100 функцій? У bundle потрапить тільки одна.

Один стандарт. Замість CommonJS для сервера, AMD для браузера і UMD для бібліотек — один стандарт для всіх. Працює нативно в браузері з <script type="module">, в Node.js, у Deno, у Bun.

Але була проблема

Стандарт опубліковано в 2015. А коли браузери його підтримали? Chrome 61 — вересень 2017. Firefox 60 — травень 2018. Safari 11 — вересень 2017 (повна таблиця підтримки). Два роки розриву між стандартом і реальністю. Тому бандлери нікуди не зникли — і Webpack продовжив царювати.

А у 2020 році TC39 додав ще одну важливу деталь — динамічний import(). На відміну від статичного import, він дозволяє завантажувати модулі за умовою, в рантаймі:

// Динамічний import — завантажує модуль тільки коли потрібно
button.addEventListener('click', async () => {
  const { openEditor } = await import('./heavy-editor.js');
  openEditor();
});

Це вирішило останню перевагу CommonJS — можливість умовного require(). Тепер ESM міг все.


Ера 8: Сучасний стан — ESM скрізь (2024–2026)

Сьогодні ES Modules — це стандарт. Не "один з варіантів", а саме стандарт. Ось як ми до цього дійшли.

Node.js: підтримка ESM з'явилась експериментально у версії 8.5.0 (вересень 2017), а стабільна — у версії 13.2.0 (листопад 2019). Щоб використовувати import/export в Node.js, потрібно або розширення .mjs, або додати "type": "module" в package.json:

{
  "name": "my-project",
  "type": "module"
}

Нові рантайми: у 2018 році Ryan Dahl — так, той самий автор Node.js — виступив з доповіддю "10 Things I Regret About Node.js" на JSConf EU. Він публічно визнав свої помилки, і одна з головних — що Node.js використовує CommonJS замість ES Modules. Як виправлення він створив Deno — рантайм, де ESM працює нативно з першого дня. Bun (Jarred Sumner, 2022) пішов тим же шляхом — ESM за замовчуванням, без конфігурації.

Vite: у 2021 році Evan You (автор Vue.js) створив Vite — інструмент, який використовує нативні ES Modules браузера під час розробки. Замість збирати весь проект в один bundle при кожній зміні — Vite просто віддає файли як є, а браузер сам завантажує модулі. Для production builds Vite використовує Rollup (пам'ятаєш Rich Harris?). Результат: миттєвий старт dev-сервера навіть у великих проєктах.

Import Maps: а ще браузери нарешті отримали Import Maps — спосіб маппити імена пакетів на URL прямо в HTML, без бандлера:

<script type="importmap">
{
  "imports": {
    "lodash": "https://cdn.jsdelivr.net/npm/lodash-es/lodash.js"
  }
}
</script>
<script type="module">
  import { debounce } from 'lodash'; // працює без Webpack!
</script>

Це не замінює бандлери для великих проєктів, але для прототипів і невеликих сайтів — магія.

2026 рік часто називають "роком повного переходу на ESM" — більшість популярних npm-пакетів вже поставляються в ESM-форматі, а CommonJS залишається переважно для зворотної сумісності.

Інфо
РікПодія
1995JavaScript створено — без модулів
2002Namespace pattern (Bindows)
2006jQuery популяризує IIFE-плагіни
2009CommonJS, Node.js, RequireJS — модульна революція
2010AMD специфікація, термін "IIFE"
2011Browserify — CommonJS у браузері
2014Webpack, TC39 фіналізує ES Module синтаксис
2015ES2015 стандарт опубліковано, Rollup вводить tree shaking
2017Браузери підтримують <script type="module">
2018Ryan Dahl створює Deno"10 помилок Node.js"
2019Node.js — стабільна підтримка ESM
2020Динамічний import() — TC39 Stage 4
2021Vite — нативні ESM у розробці
2026Рік масового переходу на ESM

Що з цього винести

30 років — від var count = 0 у глобальному просторі до елегантного import { count } from './counter.js'. Кожна ера вирішувала проблему попередньої:

  • Namespace → рятував від конфліктів, але без приватності
  • IIFE → дав приватність, але без системи залежностей
  • CommonJS → дав систему, але тільки для серверу
  • AMD → працював у браузері, але конкурував з CommonJS
  • UMD → об'єднав обидва, але потворно
  • Бандлери → зібрали все разом, але додали складність
  • Rollup → показав, що бандлер може бути елегантним, і дав нам tree shaking
  • ES Modules → стандарт, що працює всюди

Навіщо знати цю історію? Тому що ти неминуче зустрінеш require() у старому Node.js-коді, define() у legacy-проєктах, IIFE в jQuery-плагінах. І замість паніки — ти розумітимеш, звідки це взялося і чому так написано.

Хочеш розібрати import/export на практиці з прикладами? Подивись наш урок про ES6 модулі. А якщо тільки починаєш свій шлях у JavaScript — стартуй з безкоштовного курсу, де ми проведемо тебе від змінних до React.

Інфо