Интернет-магазин загружает страницу за 4 секунды. Анализ показывает: 600 KB уходит на PDF-экспортёр, редактор изображений и графики — функции, которыми пользуются менее 5% посетителей. Решение — lazy loading: грузить модули только в момент, когда они нужны.
Статический import загружает всё при старте — даже то, что пользователь никогда не откроет. import() возвращает Promise и загружает модуль по требованию:
// Статический — грузится всегда при старте (плохо для редких фич)
import { exportToPDF } from './pdf-exporter'
// Динамический — грузится только при клике на "Экспорт PDF"
const { exportToPDF } = await import('./pdf-exporter')// Возвращает Promise с объектом модуля (всеми его экспортами)
const module = await import('./path/to/module.js')
const defaultExport = module.default // default-экспорт
const { namedExport } = module // именованный экспорт
// Или короче:
const { default: Chart } = await import('./chart.js')button.addEventListener('click', async () => {
button.disabled = true
button.textContent = 'Загрузка...'
try {
// pdfMake ~500KB — грузится только при первом клике
const { default: pdfMake } = await import('pdfmake/build/pdfmake')
pdfMake.createPdf(docDefinition).download('report.pdf')
} finally {
button.disabled = false
button.textContent = 'Экспорт PDF'
}
})async function initEditor(type) {
let Editor
if (type === 'code') {
// Тяжёлый Monaco Editor — только для кода
const { MonacoEditor } = await import('./editors/monaco')
Editor = MonacoEditor
} else {
// Лёгкий текстовый редактор — для остальных случаев
const { TextEditor } = await import('./editors/text')
Editor = TextEditor
}
return new Editor(document.getElementById('editor'))
}Браузер и Node.js кэшируют модули — повторный import() того же пути возвращает кэшированный модуль мгновенно без повторной загрузки.
const a = await import('./chart.js') // сетевой запрос
const b = await import('./chart.js') // из кэша, мгновенно
console.log(a === b) // true — один и тот же объектВ ESM-модулях доступна специальная переменная:
// Текущий URL модуля (в браузере)
console.log(import.meta.url)
// 'https://example.com/src/utils/formatter.js'
// В Node.js — аналог __dirname
import { fileURLToPath } from 'url'
import { dirname, join } from 'path'
const __dirname = dirname(fileURLToPath(import.meta.url))
const configPath = join(__dirname, '../config.json')Ошибка 1: не ждут завершения загрузки
// Сломано: module ещё не загружен, Promise<module>
const module = import('./heavy-lib.js')
module.process(data) // TypeError: module.process is not a function
// Исправлено:
const module = await import('./heavy-lib.js')
module.default.process(data)Ошибка 2: забывают .default для default-экспортов
// Модуль: export default class Chart { ... }
const Chart = await import('./chart.js')
new Chart() // TypeError: Chart is not a constructor — это объект модуля!
// Исправлено:
const { default: Chart } = await import('./chart.js')
new Chart() // OKОшибка 3: динамический путь без обработки ошибки
// Если модуль не существует — промис отклоняется
const lang = userSettings.lang // 'fr' — файл не существует
const translations = await import(`./i18n/${lang}.js`) // ошибка!
// Исправлено:
try {
const translations = await import(`./i18n/${lang}.js`)
return translations.default
} catch {
const fallback = await import('./i18n/ru.js')
return fallback.default
}const Chart = React.lazy(() => import('./Chart')) — стандартный паттерн для route-based code splittingimport() на chunksawait import(./locales/${locale}.json)Менеджер фич с кэшем: загрузка тяжёлых модулей только по требованию
// Реестр фич — в реальном коде это были бы пути к файлам
const FEATURE_REGISTRY = {
'chart-builder': { path: './features/chart-builder.js', size: '320 KB' },
'pdf-exporter': { path: './features/pdf-exporter.js', size: '480 KB' },
'image-editor': { path: './features/image-editor.js', size: '890 KB' },
'markdown-editor': { path: './features/markdown-editor.js', size: '210 KB' },
}
// Кэш загруженных модулей (браузер кэширует сам, но так явнее)
const loadedFeatures = new Map()
async function loadFeature(featureName) {
// 1. Неизвестная фича — ошибка
if (!FEATURE_REGISTRY[featureName]) {
throw new Error(`Неизвестная фича: "${featureName}". Доступны: ${Object.keys(FEATURE_REGISTRY).join(', ')}`)
}
// 2. Уже загружена — возвращаем из кэша
if (loadedFeatures.has(featureName)) {
console.log(`"${featureName}" — из кэша`)
return loadedFeatures.get(featureName)
}
const { size } = FEATURE_REGISTRY[featureName]
console.log(`Загрузка "${featureName}" (~${size})...`)
// 3. Симуляция динамического import()
// В реальном коде: const module = await import(FEATURE_REGISTRY[featureName].path)
const module = await simulateImport(featureName)
loadedFeatures.set(featureName, module)
console.log(`"${featureName}" загружена`)
return module
}
// Симуляция import() (для sandbox — без файловой системы)
function simulateImport(name) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (name === 'pdf-exporter') {
// Симуляция: модуль отсутствует (не включён в сборку)
reject(new Error(`MODULE_NOT_FOUND: ${name}`))
return
}
resolve({
default: {
name,
init: (container) => console.log(` ${name}: инициализирован в #${container}`),
version: '3.0.1',
},
})
}, 200)
})
}
// Использование: загружаем фичи по требованию
async function main() {
console.log('=== Запуск приложения (лёгкий) ===')
console.log('Тяжёлые модули не загружаются при старте')
// Пользователь открыл редактор Markdown
console.log('\n--- Пользователь: Открыть редактор ---')
const mdEditor = await loadFeature('markdown-editor')
mdEditor.default.init('editor-container')
// Пользователь снова открыл — берётся из кэша
console.log('\n--- Пользователь: Снова открыть редактор ---')
await loadFeature('markdown-editor')
// Пользователь хочет PDF — модуль не найден (не включён в сборку)
console.log('\n--- Пользователь: Экспорт PDF ---')
try {
await loadFeature('pdf-exporter')
} catch (err) {
console.warn(`Фича недоступна: ${err.message}`)
console.log('Предлагаем альтернативу: экспорт в HTML')
}
// Загружаем charts
console.log('\n--- Пользователь: Открыть графики ---')
const charts = await loadFeature('chart-builder')
charts.default.init('charts-panel')
console.log(`\n=== Итого загружено модулей: ${loadedFeatures.size} из ${Object.keys(FEATURE_REGISTRY).length} ===`)
}
main()Интернет-магазин загружает страницу за 4 секунды. Анализ показывает: 600 KB уходит на PDF-экспортёр, редактор изображений и графики — функции, которыми пользуются менее 5% посетителей. Решение — lazy loading: грузить модули только в момент, когда они нужны.
Статический import загружает всё при старте — даже то, что пользователь никогда не откроет. import() возвращает Promise и загружает модуль по требованию:
// Статический — грузится всегда при старте (плохо для редких фич)
import { exportToPDF } from './pdf-exporter'
// Динамический — грузится только при клике на "Экспорт PDF"
const { exportToPDF } = await import('./pdf-exporter')// Возвращает Promise с объектом модуля (всеми его экспортами)
const module = await import('./path/to/module.js')
const defaultExport = module.default // default-экспорт
const { namedExport } = module // именованный экспорт
// Или короче:
const { default: Chart } = await import('./chart.js')button.addEventListener('click', async () => {
button.disabled = true
button.textContent = 'Загрузка...'
try {
// pdfMake ~500KB — грузится только при первом клике
const { default: pdfMake } = await import('pdfmake/build/pdfmake')
pdfMake.createPdf(docDefinition).download('report.pdf')
} finally {
button.disabled = false
button.textContent = 'Экспорт PDF'
}
})async function initEditor(type) {
let Editor
if (type === 'code') {
// Тяжёлый Monaco Editor — только для кода
const { MonacoEditor } = await import('./editors/monaco')
Editor = MonacoEditor
} else {
// Лёгкий текстовый редактор — для остальных случаев
const { TextEditor } = await import('./editors/text')
Editor = TextEditor
}
return new Editor(document.getElementById('editor'))
}Браузер и Node.js кэшируют модули — повторный import() того же пути возвращает кэшированный модуль мгновенно без повторной загрузки.
const a = await import('./chart.js') // сетевой запрос
const b = await import('./chart.js') // из кэша, мгновенно
console.log(a === b) // true — один и тот же объектВ ESM-модулях доступна специальная переменная:
// Текущий URL модуля (в браузере)
console.log(import.meta.url)
// 'https://example.com/src/utils/formatter.js'
// В Node.js — аналог __dirname
import { fileURLToPath } from 'url'
import { dirname, join } from 'path'
const __dirname = dirname(fileURLToPath(import.meta.url))
const configPath = join(__dirname, '../config.json')Ошибка 1: не ждут завершения загрузки
// Сломано: module ещё не загружен, Promise<module>
const module = import('./heavy-lib.js')
module.process(data) // TypeError: module.process is not a function
// Исправлено:
const module = await import('./heavy-lib.js')
module.default.process(data)Ошибка 2: забывают .default для default-экспортов
// Модуль: export default class Chart { ... }
const Chart = await import('./chart.js')
new Chart() // TypeError: Chart is not a constructor — это объект модуля!
// Исправлено:
const { default: Chart } = await import('./chart.js')
new Chart() // OKОшибка 3: динамический путь без обработки ошибки
// Если модуль не существует — промис отклоняется
const lang = userSettings.lang // 'fr' — файл не существует
const translations = await import(`./i18n/${lang}.js`) // ошибка!
// Исправлено:
try {
const translations = await import(`./i18n/${lang}.js`)
return translations.default
} catch {
const fallback = await import('./i18n/ru.js')
return fallback.default
}const Chart = React.lazy(() => import('./Chart')) — стандартный паттерн для route-based code splittingimport() на chunksawait import(./locales/${locale}.json)Менеджер фич с кэшем: загрузка тяжёлых модулей только по требованию
// Реестр фич — в реальном коде это были бы пути к файлам
const FEATURE_REGISTRY = {
'chart-builder': { path: './features/chart-builder.js', size: '320 KB' },
'pdf-exporter': { path: './features/pdf-exporter.js', size: '480 KB' },
'image-editor': { path: './features/image-editor.js', size: '890 KB' },
'markdown-editor': { path: './features/markdown-editor.js', size: '210 KB' },
}
// Кэш загруженных модулей (браузер кэширует сам, но так явнее)
const loadedFeatures = new Map()
async function loadFeature(featureName) {
// 1. Неизвестная фича — ошибка
if (!FEATURE_REGISTRY[featureName]) {
throw new Error(`Неизвестная фича: "${featureName}". Доступны: ${Object.keys(FEATURE_REGISTRY).join(', ')}`)
}
// 2. Уже загружена — возвращаем из кэша
if (loadedFeatures.has(featureName)) {
console.log(`"${featureName}" — из кэша`)
return loadedFeatures.get(featureName)
}
const { size } = FEATURE_REGISTRY[featureName]
console.log(`Загрузка "${featureName}" (~${size})...`)
// 3. Симуляция динамического import()
// В реальном коде: const module = await import(FEATURE_REGISTRY[featureName].path)
const module = await simulateImport(featureName)
loadedFeatures.set(featureName, module)
console.log(`"${featureName}" загружена`)
return module
}
// Симуляция import() (для sandbox — без файловой системы)
function simulateImport(name) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (name === 'pdf-exporter') {
// Симуляция: модуль отсутствует (не включён в сборку)
reject(new Error(`MODULE_NOT_FOUND: ${name}`))
return
}
resolve({
default: {
name,
init: (container) => console.log(` ${name}: инициализирован в #${container}`),
version: '3.0.1',
},
})
}, 200)
})
}
// Использование: загружаем фичи по требованию
async function main() {
console.log('=== Запуск приложения (лёгкий) ===')
console.log('Тяжёлые модули не загружаются при старте')
// Пользователь открыл редактор Markdown
console.log('\n--- Пользователь: Открыть редактор ---')
const mdEditor = await loadFeature('markdown-editor')
mdEditor.default.init('editor-container')
// Пользователь снова открыл — берётся из кэша
console.log('\n--- Пользователь: Снова открыть редактор ---')
await loadFeature('markdown-editor')
// Пользователь хочет PDF — модуль не найден (не включён в сборку)
console.log('\n--- Пользователь: Экспорт PDF ---')
try {
await loadFeature('pdf-exporter')
} catch (err) {
console.warn(`Фича недоступна: ${err.message}`)
console.log('Предлагаем альтернативу: экспорт в HTML')
}
// Загружаем charts
console.log('\n--- Пользователь: Открыть графики ---')
const charts = await loadFeature('chart-builder')
charts.default.init('charts-panel')
console.log(`\n=== Итого загружено модулей: ${loadedFeatures.size} из ${Object.keys(FEATURE_REGISTRY).length} ===`)
}
main()Напиши асинхронную функцию loadLocale(lang), которая динамически загружает файл перевода по коду языка из объекта LOCALES. Если язык не найден или загрузка провалилась — возвращает перевод для языка "ru" по умолчанию.
if (!LOCALES[lang]) lang = 'ru'; try { const m = await simulateLocaleImport(lang); return m.default } catch { const fb = await simulateLocaleImport('ru'); return fb.default }