При сборке проекта Vite использует bundler Rollup для формирования итоговых JavaScript-файлов. По умолчанию Rollup самостоятельно анализирует граф зависимостей и создаёт чанки автоматически. Такой подход подходит для большинства проектов, однако в крупных приложениях часто требуется ручной контроль над структурой сборки.
Параметр build.rollupOptions.output.manualChunks
позволяет:
Настройка располагается в vite.config.js:
import { defineConfig } from 'vite'
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
}
}
}
}
})
Без manualChunks Rollup сам создаёт зависимости:
dist/
├── index.js
├── vendor.js
├── chunk-AB12.js
└── chunk-CD34.js
Однако автоматическое разделение не всегда эффективно:
manualChunksСамый простой вариант — объект с именами чанков.
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
vue: ['vue'],
lodash: ['lodash'],
charts: ['chart.js']
}
}
}
}
})
Результат:
dist/
├── vue.js
├── lodash.js
├── charts.js
└── index.js
Теперь каждая библиотека собирается отдельно.
Наиболее распространённая практика — создание общего vendor-файла.
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: [
'vue',
'vue-router',
'pinia'
]
}
}
}
}
})
Такой подход:
Если код приложения меняется, браузер сможет использовать
кешированный vendor.js.
Некоторые зависимости значительно увеличивают размер bundle:
monaco-editorchart.jsthreemomentfirebaseИх полезно выделять отдельно.
manualChunks: {
monaco: ['monaco-editor'],
firebase: ['firebase/app', 'firebase/auth'],
charts: ['chart.js']
}
В крупных SPA можно выделять отдельные части приложения.
manualChunks: {
admin: [
'./src/pages/admin/index.js'
],
dashboard: [
'./src/pages/dashboard/index.js'
]
}
Это особенно полезно при:
manualChunksНаиболее мощный вариант — функция.
manualChunks(id) {
}
Аргумент id содержит путь к модулю.
Пример:
manualChunks(id) {
if (id.includes('node_modules')) {
return 'vendor'
}
}
Все зависимости из node_modules попадут в отдельный
чанк.
Функция позволяет создавать сложную структуру сборки.
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('vue')) {
return 'vue'
}
if (id.includes('chart.js')) {
return 'charts'
}
if (id.includes('firebase')) {
return 'firebase'
}
return 'vendor'
}
}
Результат:
dist/
├── vue.js
├── charts.js
├── firebase.js
├── vendor.js
└── index.js
UI-фреймворки часто занимают значительный объём.
manualChunks(id) {
if (id.includes('@mui')) {
return 'mui'
}
if (id.includes('antd')) {
return 'antd'
}
if (id.includes('element-plus')) {
return 'element'
}
}
Редакторы кода и WYSIWYG-компоненты обычно очень тяжёлые.
manualChunks(id) {
if (id.includes('monaco-editor')) {
return 'editor'
}
if (id.includes('ckeditor')) {
return 'ckeditor'
}
}
manualChunks(id) {
if (
id.includes('chart.js') ||
id.includes('echarts') ||
id.includes('d3')
) {
return 'charts'
}
}
Для крупных SPA удобно создавать route-based chunks.
manualChunks(id) {
if (id.includes('/pages/admin/')) {
return 'admin'
}
if (id.includes('/pages/profile/')) {
return 'profile'
}
if (id.includes('/pages/dashboard/')) {
return 'dashboard'
}
}
manualChunks особенно эффективен вместе с ленивой
загрузкой.
const AdminPage = () => import('./pages/AdminPage.vue')
При этом:
import { defineConfig } from 'vite'
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('vue')) {
return 'vue'
}
if (id.includes('firebase')) {
return 'firebase'
}
if (id.includes('chart.js')) {
return 'charts'
}
return 'vendor'
}
if (id.includes('/admin/')) {
return 'admin'
}
if (id.includes('/dashboard/')) {
return 'dashboard'
}
}
}
}
}
})
После сборки можно получить:
dist/
├── assets/
│ ├── vue.js
│ ├── vendor.js
│ ├── firebase.js
│ ├── charts.js
│ ├── admin.js
│ ├── dashboard.js
│ └── index.js
Rollup строит граф зависимостей:
App
├── Router
├── Pinia
├── Chart.js
└── Firebase
Затем:
manualChunks вмешивается в этот процесс и позволяет
вручную задавать правила группировки.
manualChunksЕсли модуль попадает под правило manualChunks, Rollup
использует именно его.
if (id.includes('vue')) {
return 'vue'
}
Даже если библиотека могла бы попасть в другой chunk, приоритет
остаётся за manualChunks.
Одна из главных причин использования manualChunks —
стабильное кеширование.
Без разделения:
app.js
После любого изменения:
app.js -> новый hash
Браузер заново скачивает весь bundle.
С разделением:
vendor.js
app.js
Изменение приложения:
vendor.js -> не меняется
app.js -> обновляется
Браузер использует кеш vendor-файла.
Ошибка многих проектов — создание одного гигантского
vendor.js.
Плохо:
manualChunks(id) {
if (id.includes('node_modules')) {
return 'vendor'
}
}
При большом количестве библиотек:
vendor.js = 3 MB
Это ухудшает:
Хороший подход:
manualChunks(id) {
if (id.includes('vue')) {
return 'vue'
}
if (id.includes('firebase')) {
return 'firebase'
}
if (id.includes('chart.js')) {
return 'charts'
}
if (id.includes('monaco-editor')) {
return 'editor'
}
if (id.includes('node_modules')) {
return 'vendor'
}
}
manualChunks действительно полезенНаиболее эффективен в проектах:
manualChunks может быть вреденИзбыточное дробление приводит к проблемам:
Плохой пример:
manualChunks(id) {
return id.split('/').pop()
}
Это создаст огромное количество мелких файлов.
Желательно:
50–300 KB gzip
Слишком маленькие:
5 KB
создают лишние запросы.
Слишком большие:
2–5 MB
замедляют загрузку.
Для анализа структуры используют:
npm install --save-dev rollup-plugin-visualizer
Подключение:
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
visualizer()
]
})
После сборки:
npm run build
создаётся HTML-отчёт.
manualChunks и tree shakingmanualChunks не отключает tree shaking.
Rollup продолжает:
Пример:
import { debounce } from 'lodash-es'
В chunk попадёт только нужный модуль.
С CommonJS иногда возникают крупные чанки.
manualChunks(id) {
if (id.includes('lodash')) {
return 'lodash'
}
}
Особенно актуально для:
moment;lodash;Vite автоматически добавляет preload-зависимости.
При правильном разделении:
index.js
├── preload vendor.js
├── preload vue.js
└── lazy charts.js
Это улучшает производительность.
Размеры можно анализировать:
npm run build
Vite показывает:
dist/assets/index.js 45.21 kB
dist/assets/vendor.js 220.11 kB
dist/assets/charts.js 480.42 kB
vue
charts
editor
firebase
vendor
admin
profile
dashboard
critical
lazy
analytics
desktop
mobile
admin
public
В monorepo часто используют:
manualChunks(id) {
if (id.includes('/packages/ui/')) {
return 'ui'
}
if (id.includes('/packages/core/')) {
return 'core'
}
}
manualChunksПри SSR разделение особенно важно:
Однако необходимо избегать различий между server/client chunk graph.
Современные протоколы лучше работают с несколькими чанками, чем HTTP/1.1.
Но даже при HTTP/2 чрезмерное дробление остаётся проблемой:
200 маленьких чанков
всё ещё хуже, чем:
10–20 хорошо организованных файлов
Крупное приложение:
vue.js
vendor.js
charts.js
editor.js
firebase.js
admin.js
dashboard.js
profile.js
Преимущества: