Webpack — классический бандлер
Webpack собирает «пакет» (bundle): берёт точку входа, обходит все import-ы и собирает итоговый JS/CSS для браузера. Это стандарт индустрии, который живёт в тысячах проектов — его знание обязательно для работы с легаси-кодовой базой.
1. Зачем нужен бандлер
Браузер давно умеет в ES-модули, но реальной команде нужны:
- транспиляция TypeScript/JSX/Babel в понятный браузеру JS;
- обработка CSS, Sass, картинок, шрифтов в едином графе зависимостей;
- оптимизация: минификация, tree-shaking, разбивка на чанки;
- dev-сервер с горячей перезагрузкой.
2. Установка и базовый конфиг
npm init -y
npm install --save-dev webpack webpack-cli// webpack.config.js
const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.js",
clean: true
},
mode: "development"
};// package.json
"scripts": {
"build": "webpack",
"watch": "webpack --watch"
}Точка входа — файл, с которого Webpack начинает обход графа зависимостей. Всё, что импортируется из неё (прямо или косвенно), попадёт в бандл.
3. Режимы: development и production
| mode | Назначение | Оптимизации |
|---|---|---|
| development | скорость сборки, читаемый код | source maps, комменты |
| production | минимальный размер | минификация, tree-shaking, хэши |
npm run build -- --mode productionМинификация в Webpack 5 по умолчанию — TerserPlugin, CSS-минификация — CssMinimizerPlugin.
4. Loaders
Loaders преобразуют файлы по расширению и «присоединяют» их к графу модулей:
module: {
rules: [
{
test: /\.css$/,
use: ["style-loader", "css-loader"]
},
{
test: /\.(png|jpe?g|svg)$/i,
type: "asset/resource"
}
]
}// use выполняется справа налево:
// css-loader парсит CSS в JS-модуль, style-loader добавляет его в DOM
import "./styles.css";style-loader → css-loader (справа налево в массиве use). Переставь — и стили не применятся.5. Plugins
Plugins делают то, что loaders не умеют: генерируют HTML, очищают папки, инжектят переменные окружения:
npm install --save-dev html-webpack-pluginconst HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
plugins: [
new HtmlWebpackPlugin({ template: "./src/index.html" })
]
};Плагин автоматически вставит <script src="bundle.js"> в HTML. Популярные плагины: MiniCssExtractPlugin (отдельный CSS-файл), DefinePlugin (переменные), CleanWebpackPlugin.
6. Sass, CSS Modules и картинки
module: {
rules: [
{
test: /\.s[ac]ss$/i,
use: ["style-loader", {
loader: "css-loader",
options: { modules: true } // CSS Modules
}, "sass-loader"]
}
]
}import styles from "./Card.module.scss";
<div className={styles.card}>Карточка</div>import logo from "./assets/logo.png";
<img src={logo} alt="лого" />7. DevServer
npm install --save-dev webpack-dev-serverdevServer: {
static: "./dist",
port: 3000,
hot: true, // HMR
historyApiFallback: true,// SPA-роутинг
proxy: { "/api": "http://localhost:8080" }
}Запусти webpack serve — код обновится без перезагрузки страницы.
8. Entry точки и code splitting
Несколько точек входа — например, страницы или отдельные модули:
entry: {
main: "./src/index.js",
admin: "./src/admin.js"
},
output: {
filename: "[name].[contenthash].js"
}Общий код выносится автоматически в отдельный чанк:
optimization: {
splitChunks: {
chunks: "all",
cacheGroups: {
vendor: { test: /node_modules/, name: "vendor" }
}
}
}[contenthash]) заставляет браузер перекачать бандл только когда он реально изменился — это основа кэширования.9. TypeScript и Babel
Для TypeScript используем ts-loader:
npm install --save-dev typescript ts-loader{
test: /\.tsx?$/,
use: "ts-loader",
exclude: /node_modules/
},
// resolve
resolve: { extensions: [".tsx", ".ts", ".js"] }Для React+JSX через Babel: babel-loader с пресетом @babel/preset-react. Подробности по TS — в курсе TypeScript.
10. Оптимизация продакшена
mode: "production",
optimization: {
minimize: true,
usedExports: true, // tree-shaking
splitChunks: { chunks: "all" }
},
performance: {
hints: "warning"
}- tree-shaking — выкидываются неиспользуемые экспорты;
- минификация — сжатие кода и имён переменных;
- splitChunks — разбиение на чанки для параллельной загрузки.
import/export). Помечай библиотеки как sideEffects: false, иначе бандл может включить лишний код.11. Переменные окружения
Режим передаётся через --mode, а значения — через DefinePlugin или плагин dotenv-webpack:
const Dotenv = require("dotenv-webpack");
module.exports = {
plugins: [ new Dotenv() ]
};// .env
API_URL=https://api.example.comВ коде значение подставляется статически:
const api = process.env.API_URL; // заменится на строку при сборке.env.local и не коммить, а прави не зашивай в build.12. Анализ размера бандла
После сборки измеряй результат:
npm install --save-dev webpack-bundle-analyzer
// webpack.config.js
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");
module.exports = {
plugins: [ new BundleAnalyzerPlugin() ]
};Откроется интерактивная карта модулей — видно, что съедает килобайты. Дальше оптимизируй:
- сокращай импорты:
import { get } from "lodash-es"вместо всего lodash; - разбивай чанки через
splitChunks(раздел 8); - оставляй source maps только в development — в production они раздувают бандл.
webpack --profile --json > stats.json даёт статистику без плагина; сравнивай размеры до и после каждой правки.13. Module Federation
Module Federation (Webpack 5) позволяет приложениям обмениваться кодом в рантайме: одна команда выставляет «микро-приложение», другая подключает его без пересборки:
const { ModuleFederationPlugin } = require("webpack");
new ModuleFederationPlugin({
name: "host",
remotes: {
"orderApp": "orderApp@https://order.example.com/orderApp.js"
},
shared: { react: { singleton: true }, "react-dom": { singleton: true } }
})const OrderBlock = React.lazy(() => import("orderApp/OrderBlock"));- shared — модули, которые не должны дублироваться (react, vue);
- singleton: true — единый экземпляр на всех потребителей;
- как и в микрофронтенд-подходах, договаривайся о версиях зависимостей заранее.
14. resolve: алиасы и порядок поиска модулей
Короткие алиасы избавляют от лесенок ../../ и ускоряют разрешение импортов:
resolve: {
alias: { "@": path.resolve(__dirname, "src") },
extensions: [".tsx", ".ts", ".js", ".vue"],
modules: [path.resolve(__dirname, "src"), "node_modules"]
}// в коде
import { Button } from "@/ui/Button";- алиас с «точным концом»
@$совпадает только целиком, без подстановки суффикса; - порядок в extensions важен: для файла с одинаковыми именами .tsx и .js выиграет первый в списке;
- алиасы часто дублируют paths из tsconfig — держи оба файла синхронизированными, иначе редактор будет верно показывать пути только там, откуда смотрит.
15. Source maps: отладка «собранного» кода
Source map связывает минифицированный бандл с исходниками, чтобы точки останова в DevTools ставились в вашем коде, а не в однострочном минифите:
devtool: "source-map" // для продакшена
devtool: "eval-source-map" // для быстрой разработки
devtool: "hidden-source-map" // карта без ссылки: для Sentry| Режим | Скорость | Для чего |
|---|---|---|
| eval | очень быстрая | dev без полной карты |
| eval-source-map | быстрая | dev с точными исходниками |
| source-map | медленнее | production + отладка |
| nosources-source-map | медленная | production без отдачи исходного кода |
16. Скорость сборки: persistent cache и watch
Медленная пересборка съедает часы команды. Быстрые оптимизации config:
cache: { type: "filesystem" }, // кэш между запусками
watchOptions: { ignored: ["**/node_modules"] },
output: { pathinfo: false },
optimization: { usedExports: "global" }- filesystem cache важен в CI — без него каждая сборка начинается с нуля;
webpack --watchпересобирает только изменившиеся модули;- babel-loader стоит включить
cacheDirectory, а ts-loader —transpileOnly: типы проверяются отдельно.
time webpack --mode development. Если сборка выросла после добавления пакета — смотри bundle-analyzer (раздел 12), возможно, он тянет лишнее.17. Безопасность: уязвимости и целостность бандла
Webpack не знает, какие пакеты вы собираете, — риск приходит из node_modules. Основы гигиены:
npm audit --omit=dev // только прод-зависимости
npm audit fix // безопасные обновления- если есть риск с внешним скриптом, проверяй его SRI-хэш (integrity в атрибуте script);
- уязвимые dev-зависимости тоже чини: простой докер-джоб может «заминировать» NPM-проект;
- не тяни библиотеку целиком — импортируй только нужные части, это уменьшает и размер, и поверхность атаки.
npm audit fix прогони тесты и посмотри diff в lock-файле, прежде чем мёржить.18. Качество кода внутри сборки
Линтер и проверка типов не должны жить отдельным процессом: их подключают прямо к бандлеру, чтобы сборка падала на ошибках, а не пропускала их до продакшена:
npm install -D eslint-webpack-plugin fork-ts-checker-webpack-pluginconst ESLintWebpackPlugin = require("eslint-webpack-plugin");
const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin");
module.exports = {
plugins: [
new ESLintWebpackPlugin({ extensions: ["js", "ts", "vue"] }),
new ForkTsCheckerWebpackPlugin()
],
module: {
rules: [{ test: /\.ts$/, use: [{ loader: "ts-loader", options: { transpileOnly: true } }] }]
}
};eslint-webpack-pluginпоказывает ошибки в консоли и наложении devServer;transpileOnly+ ForkTsChecker отделяет трансляцию от проверки типов — сборка ускоряется;- в CI добавь
failOnError, чтобы пайплайн падал на любой ошибке линта.
19. Кроссбраузерность: browserslist и полифиллы
Webpack сам не знает, какие браузеры нужны вашему проекту. Список поддерживаемых версий передаётся через browserslist, а его читают Babel и Autoprefixer:
// package.json
"browserslist": ["defaults", "not IE 11", "last 2 Chrome versions"]{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: { presets: [["@babel/preset-env", { useBuiltIns: "usage", corejs: 3 }]] }
}
}useBuiltIns: "usage"подставляет полифиллы только под реально используемые фичи;corejs: 3берёт современные полифиллы изcore-js;- тот же browserslist использует Autoprefixer для вендорных префиксов в CSS.
npm i -D core-js autoprefixer postcss-loaderfetch, Promise и Object.entries для старых браузеров: Babel транспилирует синтаксис, но не все рантайм-API.20. Кэш клиента: contenthash и runtimeChunk
Самая бесплатная оптимизация — правильные имена файлов. Когда бандл выходит с новым хэшем, браузер качает только изменившееся:
output: {
filename: "[name].[contenthash:8].js",
chunkFilename: "[name].[contenthash:8].chunk.js"
},
optimization: {
moduleIds: "deterministic",
chunkIds: "deterministic",
runtimeChunk: "single"
}[contenthash]меняется только при реальном изменении содержимого;runtimeChunk: "single"выносит загрузчик модулей в отдельный файл — он не инвалидирует vendor;- детерминированные id не «ломают» кэш, когда к проекту добавляют новый модуль.
// подключение длинноживущих чанков в index.html
21. Воспроизводимые сборки и чек-лист продакшена
«Локально собралось, а в CI упало» — следствие плавающего окружения. Три правила, которые делают сборку воспроизводимой:
# в CI вместо npm install
npm ci
# переиспользование кэша между джобами
cache: webpack filesystem cache
# артефакт для выкладки
upload-artifact: distnpm ciустанавливает ровно то, что в lock-файле — никакого дрейфа версий;- file-кэш webpack ускоряет повторные сборки в пайплайне;
- финальный dist не должен содержать секретов и случайных файлов.
| Проверка | Чем убедиться |
|---|---|
| Хэши стабильны | две сборки без изменений идентичны |
| Нет sourcemap в проде | отключён devtool, кроме внутренних нужд |
| Нет ключей в бандле | grep по env-плейсхолдерам не находит значений |
npm audit и сверяй итоговые размеры с предыдущей версией — безопасность и перформанс проверяются здесь, а не на бою.Что дальше
В новых проектах чаще используют Vite — курс Vite. Но Webpack-конфиг встретится в легаси, поэтому потренируйся: собери проект на React или Vue 3 с TypeScript, подключи Sass и ESLint, а результат задеплой на GitHub.