Циклические зависимости в модульной системе возникают тогда, когда два или более модуля прямо или косвенно зависят друг от друга. В контексте сборщика Webpack это одна из наиболее сложных категорий проблем, поскольку она затрагивает порядок инициализации модулей, кеширование экспорта и поведение runtime-обвязки.
В экосистеме ES Modules и CommonJS цикл появляется, когда граф зависимостей перестаёт быть ацикличным:
На уровне исполнения это приводит к частично инициализированным
модулям. В случае ES Modules экспорт существует, но его значения могут
быть не до конца заполнены на момент обращения. В CommonJS ситуация
выражена через module.exports, который может возвращать
незаполненный объект или undefined свойства.
Webpack строит внутренний граф модулей, где каждый файл — вершина, а импорт — ребро. При наличии цикла граф перестаёт быть DAG (Directed Acyclic Graph), и порядок выполнения становится неоднозначным.
Внутри Webpack используется ленивое выполнение модулей через функцию-обёртку. Каждый модуль кэшируется после первого вызова:
installedModulesrequire или import
возвращается кешПри циклической зависимости происходит следующее:
Это ключевой источник багов, особенно когда экспортируются функции, зависящие от ещё не инициализированных значений.
Самый частый источник циклов — агрегирующие файлы:
// index.js
export { a } from './a';
export { b } from './b';
// a.js
import { b } from './index';
Здесь создаётся косвенный цикл через re-export. Особенно часто это проявляется в больших UI-библиотеках и feature-based архитектурах.
// userService.js
import { logAction } from './logger';
export function getUser() {
logAction('getUser');
}
// logger.js
import { getUser } from './userService';
export function logAction(action) {
if (action === 'init') {
getUser();
}
}
Логирование и бизнес-логика часто образуют скрытые циклы.
В приложениях на React циклы часто возникают между:
Особенно опасны циклы через shared index-файлы и глобальные реэкспорты.
undefined is not a functionВ сборке Webpack такие ошибки часто трудно локализуются, потому что сам бандл не падает — ломается логика инициализации.
Для анализа циклов используется плагин circular-dependency-plugin. Он интегрируется в процесс сборки и отслеживает появление циклических связей в графе модулей.
Базовая конфигурация:
const CircularDependencyPlugin = require('circular-dependency-plugin');
module.exports = {
plugins: [
new CircularDependencyPlugin({
exclude: /node_modules/,
failOnError: false,
allowAsyncCycles: false,
cwd: process.cwd(),
}),
],
};
При включении строгого режима (failOnError: true) сборка
превращается в механизм контроля архитектуры.
Плагин выводит цепочку:
Circular dependency detected:
src/a.js -> src/b.js -> src/c.js -> src/a.js
Ключевая задача — определить не сам цикл, а причину его появления. Обычно это:
Вместо прямого импорта используется передача зависимостей:
// logger.js
export function createLogger(getUser) {
return {
log() {
getUser();
}
};
}
Это разрывает прямую связь модулей.
Если два модуля зависят друг от друга, часто это сигнал, что они относятся к одной доменной области и требуют выделения общего слоя.
Barrel-файлы часто создают ложное ощущение чистоты API, но внутри скрывают циклы. Решение — прямые импорты:
import { a } from './a';
import { b } from './b';
вместо реэкспорта через index.
Ленивая загрузка может разорвать цикл:
export async function loadUser() {
const { getUser } = await import('./userService');
return getUser();
}
Webpack корректно разделяет такие зависимости на чанки, уменьшая вероятность раннего исполнения цикла.
В проектах с TypeScript проблема усиливается из-за:
Webpack-резолвер может по-разному трактовать пути, создавая “невидимые” циклы.
В монорепо циклы часто выходят за пределы одного пакета:
Особенно это характерно для архитектур с shared-layer, где отсутствует строгая направленность зависимостей.
Webpack в таких случаях видит цикл только на уровне итогового графа, а не пакетов, что усложняет диагностику.
Циклические зависимости не всегда критичны по производительности, но они влияют на:
Tree-shaking в Webpack может работать некорректно, если экспорт зависит от циклически инициализированных значений.
Устойчивые к циклам системы обычно строятся по принципу направленного графа:
Любое отклонение от этого направления быстро приводит к появлению скрытых циклов, особенно в больших кодовых базах.
При работе с плагином circular-dependency-plugin полезно:
Иногда цикл проявляется только через конкретный entry chunk, что требует анализа нескольких сборок с разными точками входа.
Внутренний кеш Webpack создаёт иллюзию “устойчивости” циклов. Однако:
Это объясняет, почему ошибки могут появляться только при определённых сценариях запуска приложения.
Циклические зависимости — это не просто технический дефект, а индикатор нарушенной структуры зависимостей в графе модулей, который Webpack вынужден исполнять строго последовательно, даже если исходный код предполагает обратное.