В основе работы 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 выделяет:
На этом этапе формируется список зависимостей текущего модуля.
После анализа первого модуля Webpack переходит к его зависимостям. Процесс повторяется рекурсивно:
Таким образом граф расширяется до тех пор, пока не будут обработаны все достижимые модули.
Внутренне Webpack использует стратегию обхода, близкую к поиску в ширину (BFS), хотя в реализации присутствуют оптимизации для параллельной обработки.
Обобщённо процесс выглядит так:
создаётся очередь модулей
начальный модуль добавляется в очередь
пока очередь не пуста:
Такой подход позволяет избежать повторной обработки модулей и корректно учитывать циклические зависимости.
Перед добавлением модуля в граф Webpack выполняет разрешение пути. Этот процесс включает:
Пример alias:
resolve: {
alias: {
'@utils': path.resolve(__dirname, 'src/utils/')
}
}
Импорт:
import helper from '@utils/helper';
В графе зависимостей это будет преобразовано в конкретный файл, например:
/src/utils/helper.js
Каждый узел графа содержит метаданные:
Упрощённо структура узла:
{
id: 1,
path: '/src/index.js',
dependencies: [
'/src/utils.js',
'/src/config.js'
],
code: 'function module(...) { ... }'
}
Loaders выполняют трансформацию модуля до или во время его включения в граф. Они могут:
Важно, что 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 использует кэширование:
Это особенно важно при больших графах с тысячами модулей.
После построения графа Webpack использует его для формирования чанков:
Пример динамического разбиения:
import('./module').then(m => m.default());
Такая зависимость добавляет отдельную ветку в графе, которая будет вынесена в отдельный файл.
Динамический import() изменяет граф во время выполнения:
Таким образом граф становится не только статическим, но и частично ленивым.
В конечном виде граф зависимостей представляет собой ориентированный ациклический граф (DAG) с учётом динамических ветвлений. Однако наличие циклов в исходном коде не нарушает корректность, так как Webpack управляет их обработкой на уровне модулей.
Основные свойства графа:
Именно этот граф становится фундаментом для дальнейших этапов сборки: оптимизации, минификации и генерации финальных бандлов.