Как работают сборщики: Webpack

Как работают сборщики: Webpack

Anastasia Kotova

Мы уже говорили о движке V8 и о запуске JavaScript на сервере и в браузере. Благодаря устройству движков и самой природе языка нам не нужна предварительная компиляция перед исполнением кода — эту работу выполняет V8 и аналогичные инструменты. Тем не менее мы продолжаем использовать промежуточную стадию сборки, прежде чем наш JavaScript попадёт в среду выполнения. При этом процессы сборки для продакшена и для режима разработки устроены по-разному.

Речь, конечно, о сборщиках. И два самых заметных представителя — Webpack и Vite. На их примерах попробуем разобраться, из чего состоят современные сборщики, какие задачи они решают и как именно это делают.

Зачем они появились

Когда-то JavaScript подключали буквально построчно — десятками отдельных файлов через теги <script>, которые загружались последовательно. С ростом сложности приложений такой подход перестал работать: код становился фрагментированным, а производительность — непредсказуемой. Первые бандлеры появились как попытка объединить всё в один файл. Сначала это были простые скрипты, затем появился Webpack, предложивший системный подход: построение графа зависимостей, подключение модулей через require и поддержку различных типов ресурсов с помощью плагинов и лоадеров.

Параллельно развивался стандарт модулей JavaScript — сначала CommonJS, позже ES Modules. Казалось бы, сборщики больше не нужны: можно просто использовать import и export прямо в браузере. Но экосистема фронтенда опередила язык. Разработчики уже привыкли к TypeScript, JSX, SCSS, PostCSS-плагинам и множеству других надстроек, которые требуют транспиляции и оптимизации до попадания кода в браузер.

Современный сборщик — это уже не просто инструмент, который «склеивает» файлы. Это система, берущая на себя почти весь цикл подготовки фронтенд-кода к исполнению. Она работает как конвейер: принимает исходники, обрабатывает каждый модуль, применяет преобразования, оптимизирует зависимости и формирует итог, готовый к запуску в браузере или на сервере. Его задачи давно выходят за рамки простого объединения файлов в один бандл.

Одна из ключевых функций — трансформация кода. Большинство проектов сегодня пишутся не на «чистом» JavaScript, а на TypeScript или JSX, используют современные синтаксические возможности, ещё не поддерживаемые всеми браузерами. Сборщик подключает транспилеры вроде Babel или esbuild, чтобы превратить современный код в совместимый. Аналогично происходит и с CSS: препроцессоры (Sass, Less) и постпроцессоры (PostCSS) становятся частью того же процесса сборки.

Вторая важная задача — построение графа зависимостей. Сборщик анализирует проект, начиная с точки входа, и определяет, какие файлы связаны между собой. Это позволяет не только понять, что нужно объединить, но и применить оптимизации вроде tree-shaking — удаления неиспользуемого кода.

Далее следует оптимизация. Сборщик умеет разбивать код на чанки, применять code splitting и lazy loading, чтобы загружать только необходимое при старте приложения. Продакшен-сборка часто проходит минификацию и сжатие, чтобы сократить размер итоговых файлов.

Отдельная категория — инструменты для разработки. Сборщики давно перестали быть утилитами только для продакшена. Они запускают dev-сервер, следят за изменениями, пересобирают изменённые части и обновляют страницу через hot reload. Такая быстрая обратная связь напрямую влияет на эффективность разработки.

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

Как работает Webpack

В основе Webpack лежит концепция графа зависимостей. Всё начинается с точки входа — главного файла проекта. Сборщик анализирует его, проходит по всем import и require, создаёт вершины графа для каждого модуля и связывает их рёбрами. Так Webpack получает полное представление о структуре приложения — какие файлы зависят друг от друга, что можно объединить, а что стоит вынести в отдельный чанк.

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

Когда нужный файл найден, Webpack применяет к нему цепочку лоадеров — функций, которые получают исходный код и возвращают преобразованный. Выполняются они справа налево, что позволяет выстраивать конвейер: sass-loader превращает SCSS в CSS, затем css-loader обрабатывает импорты и URL, а style-loader встраивает результат в JavaScript.

У лоадеров есть две фазы: pitch и основная. Pitch-фаза запускается заранее и позволяет перехватить процесс до чтения файла. Если на этом этапе лоадер возвращает результат, оставшаяся цепочка может не выполняться вовсе.

use: ['a-loader', 'b-loader', 'c-loader']

|- a-loader `pitch`
  |- b-loader `pitch`
    |- c-loader `pitch`
    |- c-loader исполнение
  |- b-loader исполнение
|- a-loader исполнение

Когда связи в графе построены, сборщик разбивает его на логические группы (чанки) для оптимизации загрузки: часть кода можно отложить и подгрузить по требованию. На финальном этапе происходят оптимизации — минификация, tree-shaking, генерация итоговых файлов.

Плагины в Webpack

Если лоадеры работают с отдельными файлами, то плагины управляют процессом целиком. Они могут вмешиваться в любой этап — анализ, оптимизацию, генерацию или запись файлов. Для этого Webpack предоставляет хуки — точки входа, где можно подписаться (.tap()) и выполнить свой код. Библиотека, которая отвечает за реализацию этой системы — Tapable. Почти все ключевые компоненты — Compiler, Compilation, NormalModuleFactory — основаны на ней и имеют собственные наборы хуков.

compiler.hooks.normalModuleFactory.tap('MyPlugin', factory => {
  factory.hooks.parser.for('javascript/auto').tap('MyPlugin', parser => {
    parser.hooks.importSpecifier.tap('MyPlugin', (statement, source, id, name) => {
      // … вмешиваемся в обработку import
    });
  });
});

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

Dev-режим в Webpack

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

При запуске dev-сервера активируется режим наблюдения (watch). Сборщик отслеживает файлы из графа зависимостей и, как только один из них меняется, помечает модуль как устаревший и запускает частичную пересборку. Файлы не записываются на диск — вся сборка живёт в памяти. Специальный слой webpack-dev-middleware перехватывает запросы к статике и отдаёт свежие бандлы напрямую из памяти, что устраняет накладные расходы на ввод-вывод и ускоряет отклик.

Второй ключевой элемент — Hot Module Replacement (HMR). Вместо полной перезагрузки страницы Webpack заменяет только изменившиеся модули. При запуске webpack-dev-server устанавливается соединение WebSocket между браузером и сервером. После пересборки Webpack формирует манифест с обновлёнными модулями и отправляет его клиенту. В браузере работает HMR-рантайм, который подменяет старые модули новыми прямо в памяти. Благодаря этому состояние приложения сохраняется, а изменения применяются мгновенно.

Не все модули поддерживают горячую замену. Webpack определяет это по наличию HMR-API в коде: если есть вызовы module.hot.accept() или import.meta.webpackHot.accept(), модуль способен принять обновление. Если таких вызовов нет, сборщик пытается передать обновление родителям по графу зависимостей. Если ни один модуль не принимает его, происходит fallback — страница перезагружается полностью.

На практике HMR-логику вручную почти не пишут. За это отвечают плагины и лоадеры. Например, style-loader добавляет поддержку HMR для CSS: изменения стилей обновляются без перезагрузки страницы. В проектах на React плагин @pmmmwh/react-refresh-webpack-plugin совместно с react-refresh реализует систему, которая обновляет компоненты и сохраняет их состояние.

При этом, Webpack в dev-режиме остаётся полноценным сборщиком. Он всё так же объединяет модули в единый runtime-контекст, но делает это динамично: хранит сборку в памяти, пересобирает только изменённые части и обновляет их в браузере без потери состояния. Однако, этот режим сложнее, чем у современных инструментов вроде Vite. Там браузер напрямую импортирует файлы как ES-модули, и о нём мы поговорим уже во второй части.

Читать также

Как работают сборщики: Vite

Report Page