ДокументацияJavaScriptМодули в JavaScript: import, export, CommonJS vs ESM, tree shaking
Средний 9 мин чтения

Модули в JavaScript: import, export, CommonJS vs ESM, tree shaking

Модули JavaScript — ES Modules (import/export) и CommonJS (require/module.exports). Именованный и дефолтный экспорт, реэкспорт, динамический import и tree shaking.

модулиimportexportES ModulesCommonJSrequiretree shakingJavaScript

Зачем нужны модули

Без модулей весь код в одном файле. С модулями — каждый файл это отдельная область видимости. Вы явно указываете, что экспортируете и что импортируете.

ES Modules (ESM) — современный стандарт

Работает в браузере и Node.js. Использует import и export.

Именованный экспорт

Можно несколько из одного файла:

// math.js
export const PI = 3.14

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

export function subtract(a, b) {
  return a - b
}

Или сгруппировать в конце:

const PI = 3.14
function add(a, b) { return a + b }
function subtract(a, b) { return a - b }

export { PI, add, subtract }

Именованный импорт

// main.js
import { PI, add } from './math.js'

console.log(PI)      // 3.14
console.log(add(2, 3)) // 5

Импорт с переименованием:

import { add as sum, PI as pi } from './math.js'

sum(2, 3) // 5

Импорт всего:

import * as math from './math.js'

math.PI           // 3.14
math.add(2, 3)    // 5

Дефолтный экспорт

Один на файл. Без фигурных скобок при импорте:

// User.js
export default class User {
  constructor(name) {
    this.name = name
  }
}
// main.js
import User from './User.js'

const anna = new User('Анна')

Можно дать любое имя при импорте дефолтного экспорта:

import MyUser from './User.js' // работает

Смешанный экспорт

// api.js
export function getUsers() { /* ... */ }
export function getPosts() { /* ... */ }

export default class ApiClient {
  // ...
}
import ApiClient, { getUsers, getPosts } from './api.js'

Реэкспорт

Собрать экспорты из нескольких файлов в один:

// utils/index.js
export { add, subtract } from './math.js'
export { capitalize } from './string.js'
export { default as User } from './User.js'

Теперь можно импортировать всё из одной точки:

import { add, capitalize, User } from './utils/index.js'

Реэкспорт всего:

export * from './math.js'

Динамический import()

Загружает модуль по требованию. Возвращает Promise:

async function loadChart() {
  const { Chart } = await import('./chart.js')
  const chart = new Chart('#canvas')
  chart.render()
}

button.addEventListener('click', loadChart)

Это основа code splitting — тяжёлые модули загружаются только когда нужны.

CommonJS — формат Node.js

До ES Modules в Node.js использовался CommonJS:

// Экспорт
module.exports = {
  add(a, b) { return a + b },
  PI: 3.14,
}

// Или по одному
exports.subtract = (a, b) => a - b
// Импорт
const math = require('./math')
const { add, PI } = require('./math')

Ключевые отличия от ESM

СвойствоESM (import)CommonJS (require)
Синхронностьасинхронный (парсинг до выполнения)синхронный
Когда загружаетсяна этапе парсингав момент вызова
Можно ли условно импортироватьтолько через import()да, if (cond) require(...)
Кэшированиедада
Работает в браузереда (native)нет (только через bundler)
this на верхнем уровнеundefinedmodule.exports

Почему не стоит смешивать

// НЕ делайте так
import { foo } from './bar.js'  // ESM
const baz = require('./qux')    // CommonJS

Сборщики могут это обработать, но поведение нестабильно. Выберите один формат.

Как подключить ESM

В браузере

<script type="module" src="main.js"></script>

Без type="module" браузер не поймёт import/export.

В Node.js

В package.json:

{ "type": "module" }

Или использовать расширение .mjs для ES Modules и .cjs для CommonJS.

Tree shaking

Сборщики (Vite, Webpack, Rollup) удаляют неиспользуемые экспорты. Это работает только с ESM:

// utils.js
export function used() { console.log('используется') }
export function unused() { console.log('никто не вызывает') }
import { used } from './utils.js'

В итоговом бандле unused не попадёт — сборщик увидит, что она не импортируется.

CommonJS tree shaking не поддерживает, потому что require динамический — невозможно статически определить, что используется.

Поэтому используйте ESM — бандл будет меньше.

Паттерны организации модулей

Баррель-файл (index.js)

Собирает экспорты из папки в одну точку входа:

// components/index.js
export { default as Button } from './Button.vue'
export { default as Input } from './Input.vue'
export { default as Modal } from './Modal.vue'
import { Button, Input } from './components'

Модуль с конфигурацией

// config.js
export const API_URL = process.env.API_URL || 'http://localhost:3000'
export const TIMEOUT = 5000
export const MAX_RETRIES = 3

Модуль-утилита

// format.js
export function formatPrice(amount, currency = 'RUB') {
  return new Intl.NumberFormat('ru-RU', {
    style: 'currency',
    currency,
  }).format(amount)
}

export function formatDate(date) {
  return new Intl.DateTimeFormat('ru-RU').format(new Date(date))
}

Итог

  • ESM (import/export) — современный стандарт, работает в браузере и Node.js
  • CommonJS (require/module.exports) — формат Node.js, устаревает
  • Дефолтный экспорт — один на файл, именованных — сколько угодно
  • import() — для ленивой загрузки и code splitting
  • Tree shaking работает только с ESM — используйте import/export, а не require