Історія JavaScript-модулів — від глобального хаосу до ESM
Еволюція 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 мав два несумісних стандарти модулів:
| CommonJS | AMD | |
|---|---|---|
| Завантаження | Синхронне | Асинхронне |
| Де працює | Сервер (Node.js) | Браузер |
| Синтаксис | require() / module.exports | define() / 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 залишається переважно для зворотної сумісності.
| Рік | Подія |
|---|---|
| 1995 | JavaScript створено — без модулів |
| 2002 | Namespace pattern (Bindows) |
| 2006 | jQuery популяризує IIFE-плагіни |
| 2009 | CommonJS, Node.js, RequireJS — модульна революція |
| 2010 | AMD специфікація, термін "IIFE" |
| 2011 | Browserify — CommonJS у браузері |
| 2014 | Webpack, TC39 фіналізує ES Module синтаксис |
| 2015 | ES2015 стандарт опубліковано, Rollup вводить tree shaking |
| 2017 | Браузери підтримують <script type="module"> |
| 2018 | Ryan Dahl створює Deno — "10 помилок Node.js" |
| 2019 | Node.js — стабільна підтримка ESM |
| 2020 | Динамічний import() — TC39 Stage 4 |
| 2021 | Vite — нативні 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.
- Урок: ES6 Modules — синтаксис import/export з прикладами
- JavaScript з нуля безкоштовно — покроковий план вивчення JS
- Безкоштовний курс веб-розробки — від HTML до React, українською
- MDN: JavaScript modules — офіційна документація (англ.)
- ES modules: A cartoon deep-dive — Lin Clark — чудова візуалізація роботи модулів (англ.)
- javascript.info: Модулі — вступ до модулів (укр.)
- Ryan Dahl: Node.js — JSConf EU 2009 — доповідь, що змінила індустрію (англ.)
- Ryan Dahl: 10 Things I Regret About Node.js — чому Node.js мав обрати ESM (англ.)
- Rich Harris: Tree-shaking vs dead code elimination — як народився tree shaking (англ.)