Граф зависимостей и инвалидация кеша

Одной из ключевых причин высокой скорости работы Vite является система графа зависимостей и механизм инвалидации кеша. Вместо полной пересборки приложения Vite анализирует связи между модулями, отслеживает изменения и обновляет только те части приложения, которые действительно затронуты.

Во время запуска dev-сервера Vite строит внутренний граф модулей:

  • JavaScript-модули
  • CSS-файлы
  • Vue/Svelte/React-компоненты
  • JSON
  • WebAssembly
  • статические ресурсы
  • виртуальные модули плагинов

Каждый импорт становится ребром графа.

Пример:

// 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

Этот граф используется для:

  • HMR
  • повторной трансформации модулей
  • оптимизации запросов
  • кеширования
  • определения границ обновления
  • удаления неиспользуемых модулей
  • инвалидации результатов transform hook

ModuleGraph внутри Vite

Внутри Vite существует структура ModuleGraph.

Она хранит:

  • URL модуля
  • физический путь файла
  • список импортёров
  • список импортируемых модулей
  • состояние HMR
  • результаты трансформаций
  • метаданные плагинов
  • кешированные результаты анализа

Упрощённая модель:

class ModuleNode {
    url: string
    file: string

    importedModules: Set<ModuleNode>
    importers: Set<ModuleNode>

    transformResult
    lastHMRTimestamp
}

Каждый файл представлен отдельным узлом.

Когда браузер запрашивает модуль:

/src/main.js

Vite:

  1. проверяет наличие узла в графе;
  2. анализирует импорты;
  3. связывает зависимости;
  4. сохраняет результат;
  5. отдаёт преобразованный код.

Построение графа зависимостей

Анализ import-выражений

Во время обработки модулей Vite парсит:

import { x } from './utils.js'
import './styles.css'

После этого:

  • создаются связи;
  • фиксируются зависимости;
  • формируются importer/imported relations.

Динамические импорты

Vite также отслеживает:

const module = await import('./feature.js')

Такие связи помечаются отдельно, поскольку модуль загружается лениво.

Это влияет на:

  • code splitting;
  • lazy loading;
  • HMR propagation;
  • preload-анализ.

CSS как часть графа

CSS-файлы являются полноценными узлами графа.

import './button.css'

Это позволяет:

  • обновлять стили без reload;
  • отслеживать CSS dependencies;
  • выполнять CSS HMR отдельно от JS.

Vue/Svelte/React компоненты

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

Это позволяет обновлять:

  • template отдельно;
  • styles отдельно;
  • script отдельно.

Кеширование модулей

После первой трансформации Vite кеширует:

  • AST
  • transform result
  • sourcemaps
  • dependency analysis
  • plugin results

Повторный запрос модуля больше не требует полной обработки.


Dev Server Cache

Во время работы dev-сервера:

File → Transform → Cache → Response

При повторном запросе:

File → Cache → Response

Это резко уменьшает время ответа.


HTTP-кеш браузера

Vite активно использует browser cache.

Для зависимостей из node_modules генерируются версии:

/node_modules/.vite/react.js?v=f3sf2a

Параметр v= связан с hash содержимого.

Если файл не изменился:

  • браузер использует кеш;
  • повторная загрузка не выполняется.

Dependency Pre-Bundling Cache

При pre-bundling зависимости кешируются в:

node_modules/.vite

Например:

node_modules/.vite/deps/react.js

Vite переиспользует эти результаты между перезапусками.


Инвалидация кеша

Инвалидация — процесс удаления устаревших данных после изменения файлов.

Без неё приложение продолжало бы использовать старые версии модулей.


Как работает инвалидация

При изменении файла:

utils.js changed

Vite:

  1. находит узел в ModuleGraph;
  2. помечает transform cache как invalid;
  3. очищает plugin cache;
  4. пересчитывает зависимости;
  5. инициирует HMR propagation.

Частичная инвалидация

Одно из главных преимуществ Vite — отсутствие полной очистки всего кеша.

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

Пример:

A imports B
B imports C

Изменение C:

invalidate(C)

может затронуть:

C → B → A

Но остальные части приложения останутся нетронутыми.


Распространение инвалидации вверх

Vite поднимается по цепочке importer’ов:

Child → Parent → Root

Пока не найдёт HMR boundary.


HMR Boundary

Граница HMR — модуль, способный принять обновление самостоятельно.

Например:

if (import.meta.hot) {
    import.meta.hot.accept()
}

Такой модуль останавливает дальнейшее распространение обновления.


Если HMR boundary не найден

Если Vite не может безопасно обновить модуль:

Full Reload

Это происходит при:

  • изменении export shape;
  • изменении side effects;
  • нарушении dependency consistency;
  • изменении конфигурации.

Инвалидация CSS

CSS обновляется отдельно.

При изменении:

.button {
    color: blue;
}

Vite:

  • пересобирает CSS;
  • обновляет <style>;
  • не трогает JS;
  • не выполняет reload страницы.

Инвалидация Vue SFC

Для Vue-компонентов инвалидация более точечная.

Изменение:

<style>

обновляет только стили.

Изменение:

<template>

перегенерирует render function.

Изменение:

<script>

может вызвать remount компонента.


Timestamp-based invalidation

Vite отслеживает время изменения файлов.

mtime

Если timestamp изменился:

cache invalid

Это дешёвый и быстрый способ проверки актуальности.


Import Analysis Cache

Vite кеширует результаты анализа импортов.

Например:

import React from 'react'

После первого анализа:

  • import map сохраняется;
  • повторный parse не нужен.

При изменении файла кеш сбрасывается.


Plugin Cache Invalidation

Плагины могут хранить собственные результаты transform.

Пример:

transform(code, id)

Если файл обновился:

plugin transform cache invalidated

Плагин получает новый вызов transform.


Recursive Invalidation

Иногда обновление одного модуля требует каскадной очистки.

Например:

theme.css
  ↓
Button.vue
  ↓
App.vue

Изменение theme.css может затронуть все связанные компоненты.


Soft invalidation

Vite использует мягкую инвалидацию там, где возможно.

Это означает:

  • граф не пересоздаётся полностью;
  • удаляется только transform result;
  • связи модулей сохраняются.

Hard invalidation

Жёсткая инвалидация используется при:

  • изменении структуры импортов;
  • удалении файлов;
  • изменении alias;
  • обновлении vite.config.js;
  • изменении plugin chain.

Тогда часть графа строится заново.


Удаление модулей из графа

Если файл исчезает:

rm component.js

Vite:

  • удаляет ModuleNode;
  • очищает importer links;
  • удаляет transform cache;
  • инициирует reload.

Circular Dependencies

Граф поддерживает циклические зависимости.

Пример:

A → B
B → A

Vite отслеживает уже посещённые узлы, чтобы избежать бесконечной рекурсии.


Оптимизация обхода графа

Для быстрого поиска используются:

  • Set
  • Map
  • WeakMap

Это позволяет выполнять:

  • lookup за O(1);
  • быстрый traversal;
  • дешёвую инвалидацию.

Memory Cache

Большая часть кешей хранится в памяти процесса Node.js.

Преимущества:

  • минимальные задержки;
  • отсутствие дисковых операций;
  • быстрый HMR.

Недостаток:

  • рост потребления RAM на больших проектах.

File Watcher и инвалидация

Vite использует файловые наблюдатели:

  • chokidar
  • fs.watch
  • platform watchers

При изменении файла:

watch event

запускается pipeline:

file changed
    ↓
module invalidation
    ↓
dependency traversal
    ↓
hmr update

Почему HMR в Vite настолько быстрый

Главная причина — работа не со всем приложением, а только с небольшим подграфом зависимостей.

При изменении файла Vite:

  • не выполняет полный bundle;
  • не пересобирает весь проект;
  • не перезапускает приложение.

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


Сравнение с bundler-first архитектурой

Традиционный bundler:

Change file
    ↓
Rebuild bundle
    ↓
Emit assets
    ↓
Reload

Vite:

Change file
    ↓
Invalidate module
    ↓
Update dependency graph
    ↓
HMR

Разница особенно заметна на больших проектах.


Граф зависимостей и плагины

Плагины могут:

  • добавлять виртуальные модули;
  • изменять зависимости;
  • создавать новые import relations.

Пример виртуального модуля:

virtual:routes

Он тоже становится узлом графа.


Виртуальные модули

Плагин может возвращать:

resolveId(id) {
    if (id === 'virtual:data') {
        return '\0virtual:data'
    }
}

Такой модуль существует только внутри Vite.

Но:

  • участвует в HMR;
  • кешируется;
  • инвалидируется;
  • имеет importer/imported связи.

SSR и отдельный граф

В режиме SSR Vite может поддерживать отдельный граф модулей.

Причина:

  • серверные зависимости отличаются;
  • client/server transform различаются;
  • условия resolve отличаются.

Dependency Graph Traversal

Обход графа используется для:

  • поиска HMR boundaries;
  • recursive invalidation;
  • dependency updates;
  • pruning.

Обычно применяется DFS traversal.


Pruning неиспользуемых модулей

Если модуль больше никем не импортируется:

orphan module

Vite может удалить его из графа и очистить связанные кеши.


invalidateModule()

Во внутреннем API существует логика вида:

invalidateModule(mod)

Она:

  • очищает transformResult;
  • помечает модуль dirty;
  • запускает propagation;
  • обновляет HMR state.

Связь между графом и HMR

HMR в Vite полностью опирается на ModuleGraph.

Без графа невозможно:

  • понять, кто зависит от файла;
  • определить безопасную границу обновления;
  • вычислить affected modules;
  • минимизировать обновление.

Почему Vite масштабируется лучше старых dev-server решений

На больших проектах критична не скорость одной сборки, а стоимость повторных изменений.

Vite минимизирует:

  • объём повторной работы;
  • число transform операций;
  • объём пересчитываемого кода;
  • количество invalidated modules.

Именно граф зависимостей вместе с точечной инвалидацией кеша делает возможным практически мгновенный HMR даже в крупных frontend-приложениях.