Глубокое погружение в работу импортов JavaScript
Рассмотрим различные типы импортов, способы их организации, частые проблемы (например, циклические зависимости) и инструменты для их решения. Дадим практические рекомендации по использованию алиасов, сортировке импортов и автоматизации процессов для улучшения читаемости и эффективности кода.
359 открытий2К показов
Неорганизованное использование импортов часто приводит к проблемам с циклическими зависимостями и увеличению размера приложения.
Этого можно избежать, если подойти к организации импортов системно.
В статье мы разложим по полочкам:
- Основные типы импортов и их влияние на сборку;
- Известные способы импортировать модули;
- Подходы по организации импортов;
- Частые проблемы с импортами и инструменты их решения.
Статья будет полезна как новичкам, желающим разобраться в основах, так и сеньорам, которые хотят систематизировать свои знания и узнать о тонкостях работы импортов.
Как импорты влияют на производительность вашего приложения?
Понимание работы импортов способствует созданию лёгкого и производительного web-приложения, в то время как неправильное использование — приводит к попаданию в сборку неиспользуемого кода, что напрямую влияет на время загрузки приложения.
В чем разница CommonJS и ES Modules?
CommonJS
CommonJS (далее CJS) — система импортов, которая долгое время была стандартом в Node.js.
Синтаксис
Особенности
Загрузка модулей в CJS происходит синхронно. Это хорошо подходит для сервера, где файловая система работает быстро, но в браузере такой подход является узким местом, особенно без использования сборщиков.
По той же причине CJS не подходит для SSR, поскольку блокирует рендеринг и замедляет отдачу конечной страницы.
Интересно: Хотя современные React-приложения используют ES-модули, многие библиотеки из экосистемы NPM до сих пор пишутся или транспилируются в CommonJS. Понимание различий между этими системами может вам понадобиться для отладки получения более эффективного tree-shaking
Гибкость
require() — это обычная функция, которая может быть вызвана в любом месте кода, что само по себе довольно удобно:
CJS-модуль выполняется один раз — при первом require(). Затем результат выполнения кешируется, и все последующие вызовы будут возвращать тот же объект.
ES Modules
ES Modules (дале ESM) — современная система модулей в JavaScript, которая пришла на замену CJS.
Node.js поддерживает ESM начиная с версии 12.
Важно: Расширение .mjs является рекомендованным расширением для NodeJS, которое явно указывает разработчику, что файл содержит ESM-модуль.
Интересно: ESM это официальная спецификация ECMAScript, в отличие от CJS, который по факту был решением сообщества NodeJS.
Синтаксис
Особенности
- ESM грузится асинхронно;
- Импорты должны находиться в начале ESM — такое статическое расположение позволяет сборщикам проводить более эффективный tree-shaking;
- ESM исполняется в строгом режиме и имеет собственную область видимости;
- ESM поддерживает динамический импорт, который возвращает модуль в виде промиса.
Интересно: Библиотеки часто публикуют dual-пакеты, то есть когда библиотека собирается и в CommonJS (.cjs), и в ES-модули (.mjs), чтобы быть более универсальными. Это может пригодится когда вы захотите оптимизировать сборку своего приложения.
Также ESM предоставляет объект import.meta для получения информации о модуле. В нём доступны свойства url dirname filename, которые содержат URL, директорию и имя файла модуля соответственно.
ESM можно подключать нативно, прямо в браузере:
Import Maps
Import Maps позволяют контролировать разрешение импортов в браузере, аналогично тому как это делают сборщики:
Import Maps обычно использую для:
- Алиасов;
- Версионирования;
- Когда выбор библиотеки зависит от окружения.
Также, Import maps очень полезен для быстрых прототипов на сервисах где можно пошарить какой-то компонент, чтобы не тащить сборку, например на Codepen.
Как можно импортировать модули?
В JavaScript существует множество видов импорта, каждый из которых служит своей цели и имеет конкретные особенности:
Именованный
Особенности:
- Точное указание импортируемых сущностей;
- Хорошо совместим с tree-shaking;
- Необходим для hot-reloading в dev-сервере.
Применение:
- Компоненты. утилиты, константы.
Важно: tree-shaking особенно хорошо работает при подходе один модуль = один файл.
Дефолтный
Особенности:
- Один основной экспорт на модуль;
- Возможность переименования при импорте;
- Плохо совместим с tree-shaking.
Возможные проблемы:
- Содержимое импорта может быть непонятным, до момента пока не заглянешь во внутрь;
- Один и тот же модуль может импортироваться под разными именами в разных файлах проекта, создавая путаницу;
- Слабая поддержка IDE.
Применение:
- Верхнеуровневые сущности;
- Ленивая загрузка модулей через React.lazy.
Комбинированный
Особенности:
- Совмещает импорт по умолчанию и именованный;
- Важен порядок объявления.
Применение:
- Модули с основным и вспомогательными экспортами.
Namespace-импорты
Применение:
- Удобен для конфигов, API, утилитарных функций.
Возможные проблемы:
- Легко поломать tree-shaking внутри модуля.
Здесь не стоит сильно пугаться что подход может поломать tree-shaking, чаще всего неиспользуемые функции, на которые нет ссылок вырезаются минификатором кода, так что если что-то в этом случае осталось в бандле, дело скорее всего в том, что функцию где-то есть ссылка.
Type-импорты
Особенности:
- Используются только для типов;
- Не попадают в runtime-код;
- Помогают избежать циклических зависимостей типов.
Применение:
- Импорт типов в TypeScript.
В TypeScript 5.0 появилась новая опция --verbatimModuleSyntax, призванная упростить ситуацию. Правила стали проще: все импорты и экспорты без type-модификаторов сохраняются. Всё, что использует type-модификатор, полностью удаляется.
Barrel-импорты (бочка импортов)
Особенности:
- Упрощение путей импорта за счёт централизации экспортов.
Возможные проблемы:
- Возможно снижение производительности при большом количестве реэкспортов.
Применение:
- Организация публичного API для папок и пакетов.
Относительные импорты
Особенности:
- Зависят от расположения файла на сервере или в файловой системе.
Применение:
- Локальные модули в пределах проекта.
Абсолютные импорты
Особенности:
- Независимость от структуры папок;
- Требуется настройка в сборщике или компиляторе.
Применение:
- Глобальные пути в проекте.
Как можно удобно организовать работу с импортами?
Алиасы
Алиасы это короткие и удобные сокращения для импорта модулей вместо длинных относительных путей. Алиасы делают код чище и упрощают рефакторинг.
Интересно: Автору много раз приходилось рефакторить проект с относительным путями, в ходе которого, во время переноса файлов, IDE не всегда мог корректно рассчитать новый относительный путь. Это приводило к тому что множество битых путей приходилось прописывать вручную.
Любой современный сборщик умеет работать с алиасами:
В нейминге алиасов можно встретить разные подходы:
- src/components — простой и понятный, но может быть избыточным при неглубокой вложенности;
- @/components — популярный вариант, но может конфликтовать с названиями npm-пакетов;
- $/components — наименее популярные вариант, но имеет наименьшие шансы на конфликты;
- ~/components — оптимальный вариант, широко используется, интуитивно понятен пользователям unix-систем;
- #/components — современный подход, настраивается в package.json и поддерживается в TypeScript 4.9+.
Алиасы это очень мощный инструмент для того, чтобы быстро скопировать код компонентов в проекте, располагая их на разной вложенности, не нужно думать где лежит импортируемый файл, если он часто и много используется.
Настройка алиасов через сборщик уже сегодня можно считать анти-паттерном, так как NodeJS позволяет настроить этот функционал через package.json
Паттерн разработки «реэкспорт через директорию»
Суть паттерн в создании единой точки входа для модуля через index.ts, который реэкспортирует нужные методы из вложенных директорий.
Преимущества:
- Короткие и чистые импорты;
- Группировка логически связанных модулей;
- Сокрытие внутренней структуры директорий.
Файл index.ts можно представить публичным API модуля, который отделяет его потребителей от внутренней реализации, что соответствует принципу разделения интерфейса (ISP) из SOLID.
Обычно, такой подход считается “антипатерном” в корпоративной разработке, так как чаще всего разработчики используют функцию автоимпорта в IDE, что может порождать разные пути импорта одного и того же файла. Лучше всего использовать его как некий фасад для публичного апи библиотеки, когда важна простота и лаконичность.
Сортировка импортов
Сортировка импортов улучшает читаемость кода: нужные зависимости находятся быстрее, а структура модуля становится более очевидной и предсказуемой.
Базовый порядок импортов:
Автоматизация
Сортировку импортов можно настроить как с помощью ESLint:
…так и при помощи Prettier:
Какие проблемы можно встретить при неправильном использовании импортов?
Циклические зависимости
Проблема возникает когда два модуля начинают импортировать друг друга напрямую.
Чаще всего циклические зависимости встречаются:
- В роутинге и навигации;
- В State-менеджменте.
Основной источник циклических зависимостей это отсутствие композиции в компонентах, что в свою очередь способствует бесконечной вложенности среди внутренних сущностей.
Интересно: Автор чаще всего встречался с циклическими зависимостями при организации сложного стейт-менеджмента в Effector и RxJS.
Иногда циклическая зависимость возникает, когда модуль из длинной цепочки импортов начинает экспортировать другой модуль из той же цепочки:
Когда Module D начнёт импортировать Module A вы получите циклическую зависимость.
Ну и самое главное, почему эту проблему невозможно обойти: в случае если на каком-то из шагов потребуется инициализация переменной, которая участвует в этом цикле, она вернется как undefined. Особенно если создалось “состояние гонки”, когда модуль одновременно участвует в нескольких импортах, один из которых циклический. Получается классическая проблема иголки в столе сена, а значит отлавливать и следить за ними надо как можно раньше, пока внезапно вылезший undefined не увеличил сроки разработки.
Как предотвратить?
- Использовать архитектурные паттерны и методологии которые занимаются управлением потока данных: MVC, FLUX, FSD, Domain Driven Design;
- Строго соблюдать направление зависимостей: нижние уровни не должны зависеть от верхних;
- Стремится к минимальной вложенности модулей;
- Подключить автоматическое отслеживание циклических зависимостей через линтеры.
Поломка tree-shaking и code-splitting
Чтобы tree-shaking работал корректно с библиотеками, их нужно собирать, а затем подключать правильным образом. Например, именованный импорт из lodash затянет в сборку всю библиотеку:
В то время как точный импорт добавит в сборку только используемый метод:
Неиспользуемые импорты
Обычно это подключенные модули или зависимости, которые фактически не применяются в коде. Такого рода мёртвый код создает лишнюю нагрузку на проект, следовательно это нужно держать на контроле.
Это процесс можно автоматизировать при помощи ESLint:
…и TypeScript компилятора:
Как проанализировать конечную сборку приложения?
Для анализа итоговой сборки приложения существуют специальные инструменты визуализации бандла:
Лучшие практики
- Используйте абсолютные пути глобально, относительные — локально;
- Придерживайтесь единого стиля именования алиасов;
- Избегайте barrel-файлов для тяжёлых компонентов;
- Отдавайте предпочтение именованным импортам;
- Применяйте динамические импорты для редко используемых модулей;
- Проверяйте поддержку tree-shaking в сторонних библиотеках;
- Ограничивайте глубину относительных путей;
- Группируйте импорты по типам;
- Автоматизируйте соблюдение практик с помощью линтеров.
Заключение
Организация импортов в JavaScript-приложениях — далеко не второстепенная задача, а важная часть приложения способная повлиять на скорость сборки, производительность и читаемость кода.
Я бы еще добавил, что для теоретической базы архитектуры импортов стоит почитать про 2 принципа GRASP: Low cohesion и High Coupling. Cистема должна состоять из слабо связанных сущностей, которые должны содержать близкую бизнес логику. Соблюдение этих принципов позволяет удобно переиспользовать созданные сущности, не теряя понимания об их зоне ответственности.
Если вам понравилась статья — подписывайтесь на мой телеграм. Там я рассказываю о жизни разработчика, делюсь практическим опытом и инсайтами, которые помогут вам расти как специалисту ✨
Отдельное спасибо экспертам за их комментарии: Александру Коротаеву и Роману Титову, обязательно подпишитесь на их телеграмм-каналы Трудно быть Коротаевым и 🦜 on the web.
Остались вопросы по статье? Давайте обсудим в комментариях 👇
Рекомендуем
Как упростить импорт JavaScript-модулей с помощью Node.js Subpath Imports
Подготовка окружения React-приложения: VSCode, Prettier, ESLint, Stylelint, Husky
ReactJS на изи: что реально нужно знать фронтенд-разработчику в 2025 году
359 открытий2К показов





