Skip to main content

Що таке Webpack?

Webpack - це статичний бандлер модулів для JavaScript-застосунків, який будує граф залежностей з твоїх вихідних файлів і генерує один або кілька оптимізованих бандлів для браузера.

Теорія

TL;DR

  • Webpack схожий на конвеєр: сирі файли (JS, CSS, зображення) потрапляють через точки входу, проходять через лоадери (спеціалізовані обробники) і виходять як готові до браузера бандли
  • Головна відмінність від простого склеювання файлів: Webpack розуміє import/require і будує повний граф залежностей, а не сліпо об'єднує файли
  • Чотири концепції закривають 90% питань на співбесіді: entry (де починати), output (куди писати), loaders (трансформують не-JS файли), plugins (все інше)
  • Використовуй, якщо є більше 5 JS файлів, потрібна обробка CSS або зображень, або підтримка старих браузерів
  • Пропусти для маленьких статичних сайтів або коли достатньо CDN посилань

Швидкий приклад

javascript
// webpack.config.js - мінімальна робоча конфігурація const path = require('path'); module.exports = { entry: './src/index.js', // Починаємо звідси output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist') }, mode: 'production' // Вмикає мінімізацію + tree shaking }; // Запуск: npx webpack // Результат: dist/bundle.js - один оптимізований файл з усіх модулів

Кілька вихідних файлів на вході, один оптимізований бандл на виході. Ось і вся ідея.

Як працює граф залежностей

Webpack стартує з точки входу і рекурсивно проходить кожен import та require. Кожен знайдений файл стає вузлом у графі. Якщо index.js імпортує utils.js, який імпортує helpers.js, всі три потрапляють до бандлу.

Це принципово інший підхід ніж простий конкатенатор файлів. Конкатенатор склеює файли у заданому порядку. Webpack знає, який код реально виконується, тому може вирізати те, що ніколи не використовується.

Entry, output, loaders, plugins

Ці чотири концепції питають на майже кожній співбесіді про Webpack.

Entry - вихідний файл. Для багатосторінкових застосунків можна вказати кілька точок входу.

Output визначає, куди Webpack записує бандл і як його назвати. path.resolve(__dirname, 'dist') - стандартний варіант.

Loaders трансформують файли, які Webpack не вміє читати з коробки. За замовчуванням він розуміє тільки JS і JSON. Додай babel-loader для транспіляції ES6+, css-loader для парсингу CSS-імпортів, style-loader для вставки стилів у DOM. Лоадери виконуються справа наліво в масиві use.

Plugins беруть на себе задачі, які лоадерам недоступні: генерація HTML файлів, витягування CSS в окремі файли, мінімізація. HtmlWebpackPlugin - найчастіший гість у конфігах.

Режим розробки і продакшену

javascript
// Development: читабельний код, source maps, швидкі перебудови module.exports = { mode: 'development', entry: './src/index.js' }; // Production: мінімізація Terser, tree shaking, без source maps module.exports = { mode: 'production', entry: './src/index.js' };

Забути про mode: 'production' - найпоширеніша помилка. Dev-бандл буває у 10 разів більший за продакшн. Бандл 200KB у продакшні часто виходить 2MB у dev-режимі. Без перебільшень.

Коли використовувати Webpack

  • React або Vue застосунок з багатьма модулями і ресурсами: Webpack підходить, особливо коли потрібен точний контроль над code splitting
  • Потрібна підтримка старих браузерів: поєднай Webpack з Babel для транспіляції ES6+ до ES5
  • Складний білд-пайплайн з кастомними лоадерами, кількома точками входу або module federation: Webpack тут доречний
  • Одна HTML-сторінка з двома <script> тегами: Webpack зайвий, CDN посилання швидше налаштувати
  • Новий проект зі швидкою ітерацією: Vite краще підійде завдяки миттєвому HMR

Порівняння бандлерів

ХарактеристикаWebpackParcelViteRollup
КонфігураціяПотрібна (JS/JSON)Не потрібнаМінімальнаПотрібна
Tree shakingТакТакТакВідмінний
Швидкість HMRДобраШвидкаНайшвидшаПотребує налаштування
Найкраще дляСкладних застосунків (React/Next.js)Швидких прототипівСучасних SPAБібліотек
Поріг входуВищийНизькийНизькийСередній

Як Webpack обробляє файли всередині

Компілятор Webpack (побудований на системі плагінів Tapable) стартує з точки входу, використовує Acorn для парсингу AST (абстрактного синтаксичного дерева) кожного файлу, знаходить виклики import/require, розрізняє шляхи, застосовує лоадери і формує чанки. У продакшені Terser виконує мінімізацію і видалення мертвого коду. У dev-режимі webpack-dev-server додає Express-сервер з HMR через WebSocket, щоб браузер замінював модулі без перезавантаження сторінки.

Типові помилки

1. Немає mode: 'production'

javascript
// Неправильно: без mode - режим 'none', жодної оптимізації module.exports = { entry: './src/index.js' }; // Результат: ~2MB немінімізованого бандлу // Правильно: module.exports = { mode: 'production', entry: './src/index.js' }; // Результат: ~200KB

2. Неправильний порядок лоадерів для CSS

javascript
// Неправильно: лоадери йдуть справа наліво, цей порядок ламає вставку стилів { test: /\.css$/, use: ['css-loader', 'style-loader'] } // Правильно: style-loader першим у масиві (виконується останнім) { test: /\.css$/, use: ['style-loader', 'css-loader'] }

Ця помилка дає порожню сторінку без жодного повідомлення про помилку. Дуже поширена ситуація.

3. Ігнорування publicPath при деплої на підшлях

javascript
// Неправильно: чанки вантажаться з кореня сайту, 404 на шляху /app/ output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist') } // Правильно: output: { publicPath: '/app/', filename: 'bundle.js', path: path.resolve(__dirname, 'dist') }

4. Бандлинг Node-only модулів для браузера

javascript
// Неправильно: Webpack намагається поліфілити fs, path тощо для браузера import fs from 'fs'; // Правильно: вкажи target і externals module.exports = { target: 'node', externals: { fs: 'commonjs fs' } };

Де зустрічається у реальних проектах

  • React (Create React App, Next.js): збирає JSX, CSS і зображення у vendor і app чанки
  • Vue CLI: обробляє однофайлові компоненти (.vue) через vue-loader
  • Angular CLI: компілює TypeScript і шаблони в AOT-оптимізовані бандли
  • Electron: збирає Node і браузерний код для десктопних застосунків
  • Storybook: dev-сервер з HMR для розробки компонентів

Я бачив конфіги Webpack на 200+ рядків у великих монорепо. У таких проектах міграція на Vite часто окупається швидше, ніж здається. Але для проектів на основі Create React App або старих налаштувань Webpack залишається стандартом.

Питання на співбесіді

Q: Яка різниця між loaders і plugins?
A: Loaders трансформують окремі типи файлів до того, як вони потрапляють у бандл (Babel транспілює JS, css-loader парсить CSS). Plugins працюють на рівні всього процесу компіляції і роблять те, чого лоадери не можуть: генерують HTML, витягують CSS в окремі файли, визначають глобальні константи.

Q: Як працює tree shaking у Webpack?
A: Базується на статичному аналізі синтаксису ES-модулів (import/export). Webpack позначає невикористані експорти як мертвий код, потім Terser видаляє їх під час мінімізації. Для роботи потрібен "sideEffects": false у package.json пакету і використання ES-модулів замість CommonJS.

Q: Яка різниця між code splitting і dynamic imports?
A: Code splitting через optimization.splitChunks - це рішення на рівні збірки: Webpack автоматично розділяє бібліотеки від коду застосунку. Dynamic imports (import('./module.js')) керуються в рантаймі: чанк завантажується за вимогою, коли браузер доходить до цього рядка. Обидва зменшують початковий розмір бандлу, але dynamic imports дають більше явного контролю.

Q: HMR проти live reload - яка реальна різниця?
A: Live reload перезавантажує всю сторінку при зміні файлу. HMR (Hot Module Replacement) замінює тільки змінений модуль у запущеному застосунку, зберігаючи стан компонентів. Для React-форми з 10 заповненими полями live reload стирає все; HMR зберігає дані і оновлює тільки логіку зміненого компонента.

Q: Що змінилось між Webpack 4 і Webpack 5?
A: Три речі для співбесіди: (1) Webpack 5 прибрав вбудовані Node-поліфіли (Buffer, process), тому браузерні збірки що на них покладались потребують явного налаштування. (2) ID чанків тепер детерміновані за замовчуванням, що покращує довготривале кешування. (3) API Asset Modules замінив file-loader, url-loader і raw-loader чотирма вбудованими типами ресурсів.

Приклади

Базовий: об'єднання двох файлів

javascript
// src/message.js export default 'Hello Webpack'; // src/index.js import message from './message.js'; console.log(message); // 'Hello Webpack' // webpack.config.js const path = require('path'); module.exports = { entry: './src/index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist') }, mode: 'production' }; // Запуск: npx webpack // Результат: dist/bundle.js (~1KB, обидва файли злиті і мінімізовані)

Два вихідних файли, один результат. Webpack вирішив import і об'єднав їх автоматично. Це граф залежностей у найпростішому вигляді.

Середній: React застосунок з CSS і генерацією HTML

javascript
// webpack.config.js для реального React-проекту const HtmlWebpackPlugin = require('html-webpack-plugin'); const path = require('path'); module.exports = { entry: './src/index.js', module: { rules: [ { test: /\.jsx?$/, use: 'babel-loader', exclude: /node_modules/ // Залежності не транспілюємо }, { test: /\.css$/, use: ['style-loader', 'css-loader'] // Справа наліво: парсинг, потім вставка } ] }, plugins: [ new HtmlWebpackPlugin({ template: './public/index.html' }) ], output: { filename: 'main.[contenthash].js', // Хеш для cache busting path: path.resolve(__dirname, 'dist') }, mode: 'production' };

babel-loader обробляє JSX і ES6+. css-loader парсить CSS-імпорти. style-loader вставляє стилі у <head> під час виконання. HtmlWebpackPlugin генерує dist/index.html і автоматично додає <script> тег з посиланням на хешований бандл.

Просунутий: динамічні імпорти і code splitting

javascript
// src/index.js const button = document.getElementById('load-btn'); button.addEventListener('click', () => { // Цей import() створює ОКРЕМИЙ чанк-файл // Він завантажується лише при кліку на кнопку import('./heavyComponent.js') .then(module => module.default()); }); // webpack.config.js const path = require('path'); module.exports = { entry: './src/index.js', output: { filename: '[name].bundle.js', chunkFilename: '[name].chunk.js', // Шаблон назви для async чанків path: path.resolve(__dirname, 'dist') }, mode: 'production' }; // Результат: // dist/main.bundle.js (~1KB, завантажується одразу) // dist/1.chunk.js (~50KB, завантажується тільки при кліку)

Початкове завантаження сторінки залишається мінімальним. heavyComponent.js завантажується тільки тоді, коли юзер справді натискає кнопку. Саме так великі SPA тримають швидкість початкового завантаження. У Webpack 5 невикористані чанки в продакшн-режимі видаляються автоматично.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?