Структура выходного бандла: что генерирует Webpack по умолчанию

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

Типичная структура после сборки:

project/
├── src/
│   ├── index.js
│   ├── components/
│   └── styles/
├── dist/
│   ├── main.js
│   └── index.html
├── package.json
└── webpack.config.js

Каталог src содержит исходный код, а dist — итоговые оптимизированные файлы.


Что такое выходной бандл

Выходной бандл — это результат объединения модулей, зависимостей, стилей и ресурсов в один или несколько файлов.

Webpack анализирует граф зависимостей:

index.js
 ├── utils.js
 ├── app.js
 │    └── api.js
 └── style.css

После обработки формируется единый файл:

main.js

или набор файлов:

runtime.js
vendors.js
main.js

Внутри этих файлов уже находятся все необходимые модули приложения.


Базовая настройка output

Структура выходного бандла управляется через раздел output.

Пример:

const path = require('path');

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

    output: {
        filename: 'main.js',
        path: path.resolve(__dirname, 'dist')
    }
};

Основные параметры

Параметр Назначение
filename Имя выходного файла
path Абсолютный путь к каталогу сборки
publicPath Базовый URL для ресурсов
clean Очистка каталога перед сборкой
assetModuleFilename Структура файлов ресурсов

Параметр filename

Определяет имя итогового JavaScript-бандла.

output: {
    filename: 'bundle.js'
}

Результат:

dist/
└── bundle.js

Динамические шаблоны имени файла

Webpack поддерживает специальные плейсхолдеры.

[name]

Имя entry-point.

entry: {
    app: './src/app.js',
    admin: './src/admin.js'
},

output: {
    filename: '[name].js'
}

Результат:

dist/
├── app.js
└── admin.js

[id]

Уникальный идентификатор чанка.

filename: '[id].js'

Результат:

dist/
├── 123.js
└── 456.js

Используется редко, поскольку имена становятся нечитаемыми.


[hash]

Общий hash всей сборки.

filename: '[name].[hash].js'

Результат:

main.a6f4d8c.js

При любом изменении проекта hash меняется.


[contenthash]

Hash конкретного содержимого файла.

filename: '[name].[contenthash].js'

Наиболее популярный вариант для production.

Пример:

dist/
├── main.12fa7c.js
├── vendors.98ab44.js
└── styles.4d9911.css

Если изменился только CSS, JavaScript-файлы сохранят прежний hash.

Это критически важно для кэширования браузером.


Параметр path

Webpack требует абсолютный путь.

Правильный вариант:

const path = require('path');

output: {
    path: path.resolve(__dirname, 'dist')
}

Неправильный вариант:

output: {
    path: './dist'
}

Webpack выдаст ошибку:

The provided value "./dist" is not an absolute path

Очистка выходного каталога

Без очистки старые файлы могут оставаться внутри dist.

Пример проблемы:

dist/
├── old.bundle.js
├── main.js
└── vendor.js

Для автоматической очистки используется:

output: {
    clean: true
}

Webpack удалит старое содержимое перед новой сборкой.


Структура чанков

Webpack может разбивать приложение на отдельные части — чанки.

Пример:

import('./dashboard');

После сборки:

dist/
├── main.js
├── dashboard.js
└── vendors.js

Назначение чанков

Чанк Содержимое
main.js Основная логика
vendors.js Библиотеки npm
dashboard.js Ленивая загрузка

Runtime-бандл

Webpack создаёт специальный runtime-код.

Он отвечает за:

  • загрузку модулей;
  • управление зависимостями;
  • lazy loading;
  • работу чанков.

Пример:

optimization: {
    runtimeChunk: 'single'
}

Результат:

dist/
├── runtime.js
├── main.js
└── vendors.js

Разделение vendor-кода

Webpack умеет автоматически выделять сторонние библиотеки.

Настройка:

optimization: {
    splitChunks: {
        chunks: 'all'
    }
}

Результат:

dist/
├── vendors-node_modules_react_index_js.js
├── main.js
└── runtime.js

Структура production-бандла

Типичная production-сборка:

dist/
├── js/
│   ├── runtime.a1b2.js
│   ├── vendors.f3e4.js
│   └── main.98cd.js
│
├── css/
│   └── main.12aa.css
│
├── images/
│   ├── logo.7d1f.png
│   └── bg.1ac4.jpg
│
└── fonts/
    └── roboto.91ef.woff2

Организация директорий внутри dist

Webpack позволяет формировать собственную структуру.

Пример:

output: {
    filename: 'js/[name].[contenthash].js'
}

Результат:

dist/
└── js/
    ├── main.js
    └── admin.js

Размещение asset-файлов

Для изображений, шрифтов и SVG применяется assetModuleFilename.

output: {
    assetModuleFilename: 'assets/[hash][ext][query]'
}

Результат:

dist/
└── assets/
    ├── a7f3.png
    ├── b22f.svg
    └── c8d1.woff2

Разделение по типам ресурсов

Более удобная структура:

module.exports = {
    module: {
        rules: [
            {
                test: /\.(png|jpg)$/i,
                type: 'asset/resource',
                generator: {
                    filename: 'images/[name].[hash][ext]'
                }
            },
            {
                test: /\.(woff2|ttf)$/i,
                type: 'asset/resource',
                generator: {
                    filename: 'fonts/[name].[hash][ext]'
                }
            }
        ]
    }
};

Результат:

dist/
├── images/
│   └── logo.abc123.png
│
└── fonts/
    └── roboto.def456.woff2

Назначение publicPath

publicPath определяет URL, по которому браузер получает ресурсы.

Пример:

output: {
    publicPath: '/static/'
}

Тогда:

main.js

будет загружаться как:

/static/main.js

Использование CDN

publicPath часто применяется для CDN.

output: {
    publicPath: 'https://cdn.example.com/assets/'
}

Файлы будут подключаться:

https://cdn.example.com/assets/main.js

Автоматический publicPath

Webpack поддерживает автоматическое определение:

output: {
    publicPath: 'auto'
}

Это полезно при:

  • динамической загрузке чанков;
  • микрофронтендах;
  • federation;
  • CDN;
  • асинхронных import.

Структура source map-файлов

При включении source maps:

devtool: 'source-map'

Webpack создаёт:

dist/
├── main.js
└── main.js.map

Файл .map содержит соответствие между минифицированным кодом и исходниками.


Структура после code splitting

Пример проекта:

import('./profile');
import('./settings');

Результат:

dist/
├── main.js
├── profile.chunk.js
├── settings.chunk.js
└── runtime.js

Именование lazy-loaded чанков

Без имени:

import('./dashboard');

Webpack создаёт:

432.js

С именем:

import(
    /* webpackChunkName: "dashboard" */
    './dashboard'
);

Результат:

dashboard.js

Структура development-сборки

Development-бандл обычно:

  • крупнее;
  • содержит source maps;
  • не минифицируется;
  • имеет читаемые имена.

Пример:

dist/
├── main.js
├── vendors.js
└── main.js.map

Структура production-сборки

Production-бандл:

  • минифицирован;
  • оптимизирован;
  • разбит на чанки;
  • содержит hash;
  • использует tree shaking.

Пример:

dist/
├── runtime.f13a.js
├── vendors.7bc1.js
├── main.22ef.js
└── main.22ef.js.LICENSE.txt

Файл LICENSE

Webpack может автоматически выносить лицензии.

Пример:

main.js.LICENSE.txt

Там находятся лицензии библиотек:

React
MIT License

Lodash
MIT License

Tree Shaking и структура бандла

Webpack удаляет неиспользуемый код.

Исходный модуль:

export function a() {}
export function b() {}
export function c() {}

Использование:

import { a } from './utils';

В production-бандл попадёт только:

function a(){}

Минификация выходных файлов

Production-режим:

mode: 'production'

Превращает:

function calculatePrice(price, tax) {
    return price + tax;
}

в:

function n(n,t){return n+t}

Анализ размера бандла

После сборки структура может оказаться слишком большой.

Типичный пример:

dist/
├── vendors.js 2.4 MB
├── main.js 700 KB
└── charts.js 1.1 MB

Для анализа используется:

npm install webpack-bundle-analyzer --save-dev

Влияние структуры бандла на производительность

Правильная структура позволяет:

  • уменьшить размер первой загрузки;
  • ускорить кеширование;
  • сократить сетевой трафик;
  • изолировать vendor-код;
  • отдельно обновлять чанки;
  • быстрее загружать lazy-модули.

Практическая production-конфигурация

const path = require('path');

module.exports = {
    mode: 'production',

    entry: {
        main: './src/index.js'
    },

    output: {
        filename: 'js/[name].[contenthash].js',
        chunkFilename: 'js/[name].[contenthash].chunk.js',
        assetModuleFilename: 'assets/[name].[hash][ext]',
        path: path.resolve(__dirname, 'dist'),
        clean: true,
        publicPath: '/'
    },

    optimization: {
        runtimeChunk: 'single',

        splitChunks: {
            chunks: 'all'
        }
    }
};

Результирующая структура:

dist/
├── js/
│   ├── runtime.aa12.js
│   ├── vendors.bb34.js
│   ├── main.cc56.js
│   └── profile.dd78.chunk.js
│
├── assets/
│   ├── logo.ee90.svg
│   ├── hero.ff22.jpg
│   └── font.gg44.woff2
│
└── index.html