Одной из ключевых причин высокой скорости работы Vite является система графа зависимостей и механизм инвалидации кеша. Вместо полной пересборки приложения Vite анализирует связи между модулями, отслеживает изменения и обновляет только те части приложения, которые действительно затронуты.
Во время запуска dev-сервера Vite строит внутренний граф модулей:
Каждый импорт становится ребром графа.
Пример:
// main.js
import { createApp } from './app.js'
// app.js
import { render } from './render.js'
// render.js
import './styles.css'
В результате формируется цепочка зависимостей:
main.js
└── app.js
└── render.js
└── styles.css
Этот граф используется для:
Внутри Vite существует структура ModuleGraph.
Она хранит:
Упрощённая модель:
class ModuleNode {
url: string
file: string
importedModules: Set<ModuleNode>
importers: Set<ModuleNode>
transformResult
lastHMRTimestamp
}
Каждый файл представлен отдельным узлом.
Когда браузер запрашивает модуль:
/src/main.js
Vite:
Во время обработки модулей Vite парсит:
import { x } from './utils.js'
import './styles.css'
После этого:
Vite также отслеживает:
const module = await import('./feature.js')
Такие связи помечаются отдельно, поскольку модуль загружается лениво.
Это влияет на:
CSS-файлы являются полноценными узлами графа.
import './button.css'
Это позволяет:
Single File Components разбиваются на подмодули.
Например, Vue-компонент:
<script>
export default {}
</script>
<template>
<div>Hello</div>
</template>
<style>
div {
color: red;
}
</style>
Во внутреннем графе может существовать как:
Component.vue?type=script
Component.vue?type=template
Component.vue?type=style
Это позволяет обновлять:
После первой трансформации Vite кеширует:
Повторный запрос модуля больше не требует полной обработки.
Во время работы dev-сервера:
File → Transform → Cache → Response
При повторном запросе:
File → Cache → Response
Это резко уменьшает время ответа.
Vite активно использует browser cache.
Для зависимостей из node_modules генерируются
версии:
/node_modules/.vite/react.js?v=f3sf2a
Параметр v= связан с hash содержимого.
Если файл не изменился:
При pre-bundling зависимости кешируются в:
node_modules/.vite
Например:
node_modules/.vite/deps/react.js
Vite переиспользует эти результаты между перезапусками.
Инвалидация — процесс удаления устаревших данных после изменения файлов.
Без неё приложение продолжало бы использовать старые версии модулей.
При изменении файла:
utils.js changed
Vite:
Одно из главных преимуществ Vite — отсутствие полной очистки всего кеша.
Инвалидируются только связанные модули.
Пример:
A imports B
B imports C
Изменение C:
invalidate(C)
может затронуть:
C → B → A
Но остальные части приложения останутся нетронутыми.
Vite поднимается по цепочке importer’ов:
Child → Parent → Root
Пока не найдёт HMR boundary.
Граница HMR — модуль, способный принять обновление самостоятельно.
Например:
if (import.meta.hot) {
import.meta.hot.accept()
}
Такой модуль останавливает дальнейшее распространение обновления.
Если Vite не может безопасно обновить модуль:
Full Reload
Это происходит при:
CSS обновляется отдельно.
При изменении:
.button {
color: blue;
}
Vite:
<style>;Для Vue-компонентов инвалидация более точечная.
Изменение:
<style>
обновляет только стили.
Изменение:
<template>
перегенерирует render function.
Изменение:
<script>
может вызвать remount компонента.
Vite отслеживает время изменения файлов.
mtime
Если timestamp изменился:
cache invalid
Это дешёвый и быстрый способ проверки актуальности.
Vite кеширует результаты анализа импортов.
Например:
import React from 'react'
После первого анализа:
При изменении файла кеш сбрасывается.
Плагины могут хранить собственные результаты transform.
Пример:
transform(code, id)
Если файл обновился:
plugin transform cache invalidated
Плагин получает новый вызов transform.
Иногда обновление одного модуля требует каскадной очистки.
Например:
theme.css
↓
Button.vue
↓
App.vue
Изменение theme.css может затронуть все связанные
компоненты.
Vite использует мягкую инвалидацию там, где возможно.
Это означает:
Жёсткая инвалидация используется при:
Тогда часть графа строится заново.
Если файл исчезает:
rm component.js
Vite:
Граф поддерживает циклические зависимости.
Пример:
A → B
B → A
Vite отслеживает уже посещённые узлы, чтобы избежать бесконечной рекурсии.
Для быстрого поиска используются:
Это позволяет выполнять:
Большая часть кешей хранится в памяти процесса Node.js.
Преимущества:
Недостаток:
Vite использует файловые наблюдатели:
При изменении файла:
watch event
запускается pipeline:
file changed
↓
module invalidation
↓
dependency traversal
↓
hmr update
Главная причина — работа не со всем приложением, а только с небольшим подграфом зависимостей.
При изменении файла Vite:
Обновляется только изменённый участок графа.
Традиционный bundler:
Change file
↓
Rebuild bundle
↓
Emit assets
↓
Reload
Vite:
Change file
↓
Invalidate module
↓
Update dependency graph
↓
HMR
Разница особенно заметна на больших проектах.
Плагины могут:
Пример виртуального модуля:
virtual:routes
Он тоже становится узлом графа.
Плагин может возвращать:
resolveId(id) {
if (id === 'virtual:data') {
return '\0virtual:data'
}
}
Такой модуль существует только внутри Vite.
Но:
В режиме SSR Vite может поддерживать отдельный граф модулей.
Причина:
Обход графа используется для:
Обычно применяется DFS traversal.
Если модуль больше никем не импортируется:
orphan module
Vite может удалить его из графа и очистить связанные кеши.
Во внутреннем API существует логика вида:
invalidateModule(mod)
Она:
HMR в Vite полностью опирается на ModuleGraph.
Без графа невозможно:
На больших проектах критична не скорость одной сборки, а стоимость повторных изменений.
Vite минимизирует:
Именно граф зависимостей вместе с точечной инвалидацией кеша делает возможным практически мгновенный HMR даже в крупных frontend-приложениях.