← JavaScript/Динамические импорты#129 из 383← ПредыдущийСледующий →+20 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

Динамические импорты

Интернет-магазин загружает страницу за 4 секунды. Анализ показывает: 600 KB уходит на PDF-экспортёр, редактор изображений и графики — функции, которыми пользуются менее 5% посетителей. Решение — lazy loading: грузить модули только в момент, когда они нужны.

Что решает динамический import()

Статический import загружает всё при старте — даже то, что пользователь никогда не откроет. import() возвращает Promise и загружает модуль по требованию:

// Статический — грузится всегда при старте (плохо для редких фич)
import { exportToPDF } from './pdf-exporter'

// Динамический — грузится только при клике на "Экспорт PDF"
const { exportToPDF } = await import('./pdf-exporter')

На основе предыдущих уроков

  • модули: статический import/export
  • Promise: import() возвращает Promise
  • async/await: используется для ожидания import()
  • try/catch: обработка ошибок при загрузке модуля
  • Синтаксис

    // Возвращает Promise с объектом модуля (всеми его экспортами)
    const module = await import('./path/to/module.js')
    const defaultExport = module.default      // default-экспорт
    const { namedExport } = module             // именованный экспорт
    
    // Или короче:
    const { default: Chart } = await import('./chart.js')

    Lazy loading: загрузка при клике

    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 — один и тот же объект

    import.meta.url — URL текущего модуля

    В 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
    }

    В реальных проектах

  • React.lazy + Suspense: const Chart = React.lazy(() => import('./Chart')) — стандартный паттерн для route-based code splitting
  • Vite/Webpack: автоматически разбивают бандл по import() на chunks
  • i18n: загружают переводы нужного языка: await import(./locales/${locale}.json)
  • Feature flags: фича выключена — модуль вообще не загружается
  • Примеры

    Менеджер фич с кэшем: загрузка тяжёлых модулей только по требованию

    // Реестр фич — в реальном коде это были бы пути к файлам
    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 загружает всё при старте — даже то, что пользователь никогда не откроет. import() возвращает Promise и загружает модуль по требованию:

    // Статический — грузится всегда при старте (плохо для редких фич)
    import { exportToPDF } from './pdf-exporter'
    
    // Динамический — грузится только при клике на "Экспорт PDF"
    const { exportToPDF } = await import('./pdf-exporter')

    На основе предыдущих уроков

  • модули: статический import/export
  • Promise: import() возвращает Promise
  • async/await: используется для ожидания import()
  • try/catch: обработка ошибок при загрузке модуля
  • Синтаксис

    // Возвращает Promise с объектом модуля (всеми его экспортами)
    const module = await import('./path/to/module.js')
    const defaultExport = module.default      // default-экспорт
    const { namedExport } = module             // именованный экспорт
    
    // Или короче:
    const { default: Chart } = await import('./chart.js')

    Lazy loading: загрузка при клике

    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 — один и тот же объект

    import.meta.url — URL текущего модуля

    В 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
    }

    В реальных проектах

  • React.lazy + Suspense: const Chart = React.lazy(() => import('./Chart')) — стандартный паттерн для route-based code splitting
  • Vite/Webpack: автоматически разбивают бандл по import() на chunks
  • i18n: загружают переводы нужного языка: await import(./locales/${locale}.json)
  • Feature flags: фича выключена — модуль вообще не загружается
  • Примеры

    Менеджер фич с кэшем: загрузка тяжёлых модулей только по требованию

    // Реестр фич — в реальном коде это были бы пути к файлам
    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 }

    Загружаем среду выполнения...
    Загружаем AI-помощника...