Граф зависимостей: как Webpack обходит модули

В основе работы Webpack лежит построение графа зависимостей, который представляет собой структуру, описывающую связи между модулями проекта. Каждый модуль рассматривается как узел графа, а зависимости между ними — как рёбра. Именно этот граф определяет порядок обработки файлов, их объединение и итоговую структуру бандла.


Формирование точки входа

Построение графа начинается с одной или нескольких точек входа, заданных в конфигурации:

module.exports = {
  entry: './src/index.js'
};

Каждая точка входа становится корневым узлом графа. Webpack анализирует файл, указанный в entry, и начинает рекурсивный обход всех его зависимостей.

Если указано несколько точек входа:

module.exports = {
  entry: {
    app: './src/app.js',
    admin: './src/admin.js'
  }
};

Каждая из них формирует отдельную ветку графа, при этом Webpack может объединять общие зависимости в отдельные чанки.


Разбор модуля: парсинг исходного кода

При обработке модуля Webpack не просто читает файл как текст. Он строит его абстрактное синтаксическое дерево (AST). Это позволяет точно определить, какие зависимости используются внутри кода.

Пример исходного модуля:

import utils from './utils';
const config = require('./config');

После парсинга Webpack выделяет:

  • статические импорты (ES Modules)
  • динамические require
  • потенциальные динамические import()

На этом этапе формируется список зависимостей текущего модуля.


Рекурсивное построение графа

После анализа первого модуля Webpack переходит к его зависимостям. Процесс повторяется рекурсивно:

  1. Берётся модуль из очереди обработки
  2. Анализируется его AST
  3. Извлекаются зависимости
  4. Каждая зависимость преобразуется в абсолютный путь
  5. Новый модуль добавляется в граф

Таким образом граф расширяется до тех пор, пока не будут обработаны все достижимые модули.


Алгоритм обхода

Внутренне Webpack использует стратегию обхода, близкую к поиску в ширину (BFS), хотя в реализации присутствуют оптимизации для параллельной обработки.

Обобщённо процесс выглядит так:

  • создаётся очередь модулей

  • начальный модуль добавляется в очередь

  • пока очередь не пуста:

    • извлекается модуль
    • анализируются зависимости
    • новые модули добавляются в очередь

Такой подход позволяет избежать повторной обработки модулей и корректно учитывать циклические зависимости.


Нормализация и разрешение путей

Перед добавлением модуля в граф Webpack выполняет разрешение пути. Этот процесс включает:

  • определение абсолютного пути файла
  • проверку расширений (.js, .ts, .json и др.)
  • учёт alias из конфигурации
  • обработку index-файлов в директориях
  • применение правил resolve.modules

Пример alias:

resolve: {
  alias: {
    '@utils': path.resolve(__dirname, 'src/utils/')
  }
}

Импорт:

import helper from '@utils/helper';

В графе зависимостей это будет преобразовано в конкретный файл, например:

/src/utils/helper.js

Узлы графа: структура модуля

Каждый узел графа содержит метаданные:

  • идентификатор модуля
  • абсолютный путь
  • список зависимостей
  • исходный код (после трансформаций loaders)
  • сгенерированный код (module factory)
  • зависимости с маппингом имён импортов

Упрощённо структура узла:

{
  id: 1,
  path: '/src/index.js',
  dependencies: [
    '/src/utils.js',
    '/src/config.js'
  ],
  code: 'function module(...) { ... }'
}

Роль loaders в построении графа

Loaders выполняют трансформацию модуля до или во время его включения в граф. Они могут:

  • транспилировать TypeScript в JavaScript
  • преобразовывать JSX
  • обрабатывать CSS и изображения
  • модифицировать исходный код

Важно, что loaders применяются до анализа зависимостей AST или сразу после чтения файла, в зависимости от типа loader’а.

Пример:

module: {
  rules: [
    {
      test: /\.ts$/,
      use: 'ts-loader'
    }
  ]
}

После обработки loader’ом код уже может отличаться от исходного, но Webpack анализирует именно итоговую версию.


Циклические зависимости

Граф зависимостей может содержать циклы:

// a.js
import { b } from './b';

// b.js
import { a } from './a';

Webpack не зацикливается благодаря отслеживанию уже посещённых модулей. Если модуль уже присутствует в графе, он не обрабатывается повторно, но его связь фиксируется.


Кэширование модулей

Для оптимизации Webpack использует кэширование:

  • если модуль уже обработан, он не парсится повторно
  • сохраняются результаты AST и трансформаций
  • ускоряется пересборка в watch режиме

Это особенно важно при больших графах с тысячами модулей.


Граф как основа чанкинга

После построения графа Webpack использует его для формирования чанков:

  • entry chunk — корневые точки входа
  • async chunk — динамические импорты
  • shared chunk — общие зависимости

Пример динамического разбиения:

import('./module').then(m => m.default());

Такая зависимость добавляет отдельную ветку в графе, которая будет вынесена в отдельный файл.


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

Динамический import() изменяет граф во время выполнения:

  • Webpack фиксирует точку разделения
  • создаётся отдельный подграф
  • формируется отдельный chunk

Таким образом граф становится не только статическим, но и частично ленивым.


Итоговая структура графа

В конечном виде граф зависимостей представляет собой ориентированный ациклический граф (DAG) с учётом динамических ветвлений. Однако наличие циклов в исходном коде не нарушает корректность, так как Webpack управляет их обработкой на уровне модулей.

Основные свойства графа:

  • детерминированность построения
  • отсутствие дублирования модулей
  • возможность параллельной обработки
  • поддержка статических и динамических связей

Именно этот граф становится фундаментом для дальнейших этапов сборки: оптимизации, минификации и генерации финальных бандлов.