SSR-совместимые плагины

SSR-совместимость в Vite опирается на единый плагинный механизм, где один и тот же плагин должен корректно работать в двух режимах выполнения: клиентском (браузерная сборка) и серверном (Node.js окружение для SSR). Основная сложность заключается в том, что SSR-режим предъявляет дополнительные требования к загрузке модулей, обработке зависимостей и отсутствию браузерных API.

SSR-режим в Vite запускается через отдельный пайплайн, где модули загружаются не как статические бандлы, а как граф зависимостей, исполняемый через ssrLoadModule. Это означает:

  • отсутствует DOM и браузерные глобальные объекты
  • используется Node.js module graph
  • ESM модули исполняются напрямую
  • требуется контроль побочных эффектов

Плагин, не учитывающий эти различия, может корректно работать в dev-режиме браузера, но ломаться при серверной сборке.

Плагинная модель Vite и SSR-флаги

Плагины Vite строятся на базе Rollup-подобной системы хуков:

  • resolveId
  • load
  • transform
  • configureServer
  • generateBundle

Для SSR добавляется контекст ssr, который передаётся во многие хуки через опции:

transform(code, id, options) {
  if (options?.ssr) {
    // SSR-специфичная трансформация
  }
  return code
}

Ключевое отличие SSR-плагина заключается в условной логике выполнения, зависящей от режима сборки.

SSR-совместимость через условные экспорты

Одним из базовых механизмов является разделение логики по среде выполнения:

if (import.meta.env.SSR) {
  // серверная логика
} else {
  // клиентская логика
}

Плагины Vite могут автоматически трансформировать такие условия, обеспечивая tree-shaking ненужного кода в каждом из окружений.

SSR-safe transform и исключение браузерных API

Основная проблема SSR-плагинов — использование браузерных глобальных объектов:

  • window
  • document
  • localStorage

SSR-плагин должен либо:

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

Пример трансформации:

transform(code, id, options) {
  if (options?.ssr) {
    return code.replace(/window\./g, 'undefined/* window */.')
  }
}

На практике используется AST-трансформация через esbuild или @babel/parser.

ResolveId и различия SSR-графа

В SSR-режиме модульный граф строится иначе. Плагин может влиять на резолвинг:

resolveId(source, importer, options) {
  if (options?.ssr) {
    if (source === 'some-browser-only-lib') {
      return source + '/ssr'
    }
  }
}

SSR-резолвинг часто направлен на альтернативные entry points библиотек, например:

  • module
  • main
  • exports["node"]

Vite учитывает package.json поле exports и условия import conditions.

SSR externalization и ssr.external

Плагины должны учитывать, что SSR-сборка часто внешнеет зависимости:

  • Node.js модули не бандлятся
  • нативные зависимости остаются external
  • крупные библиотеки исключаются из бандла

Плагин может влиять на это через ssr.external и ssr.noExternal.

export default {
  ssr: {
    noExternal: ['my-isomorphic-lib']
  }
}

SSR-совместимый плагин не должен ломать externalization логику, например, не должен принудительно инлайнить Node-only пакеты.

SSR loadModule и влияние плагинов

В dev SSR используется:

server.ssrLoadModule('/src/entry-server.js')

Плагины участвуют в:

  • трансформации модулей перед выполнением
  • подмене импортов
  • инъекции runtime-кода

Важно учитывать, что SSR-код исполняется сразу после трансформации, без финального бандла.

SSR transform: порядок и побочные эффекты

Порядок выполнения плагинов критичен:

  1. resolveId
  2. load
  3. transform

SSR-плагины должны избегать:

  • глобальных мутаций state
  • кэширования без учёта ssr флага
  • хранения browser-only контекста

Правильная изоляция:

const cache = new WeakMap()

transform(code, id, options) {
  const key = options?.ssr ? id + ':ssr' : id

  if (cache.has(key)) return cache.get(key)

  const result = code.toUpperCase()
  cache.set(key, result)

  return result
}

SSR-safe side effects

Одним из ключевых аспектов является контроль side effects.

SSR-режим требует:

  • отсутствие сетевых запросов при импорте модулей
  • отсутствие обращения к файловой системе в runtime (если не Node-safe)
  • детерминированность выполнения

Плагин может помечать модули как side-effect free:

moduleSideEffects(id) {
  if (id.includes('pure-lib')) return false
}

Клиент-серверное разделение внутри плагина

SSR-совместимый плагин часто имеет двойную логику:

export default function plugin() {
  return {
    name: 'example-plugin',

    transform(code, id, options) {
      if (options?.ssr) {
        return transformServer(code)
      }
      return transformClient(code)
    }
  }
}

Иногда выделяются отдельные внутренние модули:

  • serverTransform
  • clientTransform

SSR и HMR взаимодействие

В dev-режиме SSR и HMR работают параллельно:

  • HMR обновляет модули в браузере
  • SSR перезагружает серверный граф

Плагин может участвовать в HMR через:

handleHotUpdate(ctx) {
  if (ctx.modules.some(m => m.id.includes('ssr-only'))) {
    ctx.server.reload()
  }
}

SSR-плагины должны избегать неконсистентного состояния между клиентом и сервером.

Манипуляция module graph в SSR

Vite предоставляет доступ к графу модулей:

  • server.moduleGraph

SSR-плагин может использовать его для:

  • инвалидации SSR-кэша
  • анализа зависимостей
  • устранения циклических импортов
handleHotUpdate({ server, file }) {
  const mods = server.moduleGraph.getModulesByFile(file)
  mods.forEach(m => server.moduleGraph.invalidateModule(m))
}

SSR build mode и rollup hooks

В production SSR используется Rollup:

  • buildStart
  • generateBundle
  • writeBundle

SSR-плагин должен учитывать, что:

  • нет runtime загрузки
  • всё формируется в bundle
  • доступен manifest

SSR manifest и плагинное влияние

Vite SSR build генерирует manifest, содержащий:

  • client chunks
  • server entry points
  • asset mappings

Плагин может модифицировать:

generateBundle(options, bundle) {
  if (options.ssr) {
    // модификация server bundle
  }
}

Совместимость с hydration

SSR-плагины часто влияют на hydration процесс:

  • сохранение стабильного HTML
  • избегание недетерминированных значений
  • согласованность ID элементов

Ошибки возникают при:

  • различии server/client output
  • динамических импортов без синхронизации
  • runtime-only вычислениях

Типовые паттерны SSR-совместимых плагинов

1. Isomorphic transform

transform(code, id, options) {
  return options?.ssr
    ? transformForNode(code)
    : transformForBrowser(code)
}

2. Conditional import rewriting

resolveId(source, importer, options) {
  if (options?.ssr && source === 'fs/browser') {
    return 'fs/node'
  }
}

3. Guarded runtime injection

transform(code, id, options) {
  if (!options?.ssr) {
    return code + '\nconsole.log("client only")'
  }
  return code
}

Ошибки SSR-несовместимых плагинов

Часто встречаются следующие проблемы:

  • использование window в transform без проверки режима
  • кэширование без учета SSR/CSR контекста
  • изменение кода без учета hydration
  • некорректная работа с dynamic import
  • зависимость от DOM в build phase

Ключевые принципы SSR-совместимости

SSR-плагин в Vite должен придерживаться следующих ограничений:

  • разделение логики по ssr флагу
  • отсутствие браузерных зависимостей в server mode
  • детерминированные трансформации
  • изоляция состояния между режимами
  • корректная работа с module graph и externalization
  • уважение к Rollup build pipeline в production SSR

Такая модель позволяет одному плагину стабильно работать как в dev SSR, так и в production SSR без нарушения консистентности между серверным HTML и клиентской гидратацией.