Экстернализация node-зависимостей

Экстернализация node-зависимостей в Vite в контексте SSR представляет собой механизм управления тем, какие модули должны попадать в серверный бандл, а какие должны оставаться внешними и загружаться напрямую из среды выполнения Node.js. Этот процесс критичен для производительности, корректности резолва модулей и совместимости между ESM и CommonJS.

При серверном рендеринге приложение выполняется не в браузере, а в среде Node.js. Это накладывает принципиально иные ограничения:

  • доступен файловый API (fs, path, stream)
  • используются встроенные Node-модули
  • отсутствует DOM
  • резолв модулей следует алгоритмам Node.js, а не браузера

В SSR-сборке Vite формирует отдельный серверный бандл. В этот момент возникает ключевая проблема: часть зависимостей выгодно бандлить, а часть — оставлять внешними (external).

Экстернализация означает, что модуль не включается в итоговый SSR-бандл, а заменяется на require() или import во время выполнения.

Причины экстернализации зависимостей

1. Сохранение семантики Node.js

Некоторые пакеты рассчитывают на нативное поведение Node.js:

  • динамический require
  • доступ к __dirname
  • условные импорты CJS
  • загрузка бинарных аддонов (.node)

Бандлинг таких модулей может ломать их поведение.

2. Уменьшение размера SSR-бандла

SSR-бандл часто выполняется на сервере при каждом запросе или при горячем обновлении. Избыточное включение зависимостей:

  • увеличивает время старта
  • ухудшает cold start
  • усложняет HMR в dev-режиме

3. Избежание двойного бандлинга

Многие зависимости уже поставляются оптимизированными для Node.js. Повторная упаковка:

  • увеличивает накладные расходы
  • может ломать tree-shaking
  • ухудшает совместимость с CJS

Конфигурация ssr.external

Основной механизм управления экстернализацией в Vite — параметр:

export default {
  ssr: {
    external: []
  }
}

По умолчанию Vite стремится автоматически внешними делать большинство node_modules, если они не требуют трансформации.

Поведение ssr.external

ssr.external позволяет явно указать пакеты, которые должны быть исключены из бандла:

ssr: {
  external: ['lodash', 'express']
}

Это приводит к тому, что:

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

Когда использовать explicit external

  • нативные Node.js библиотеки
  • CJS-only пакеты с динамическим require
  • большие инфраструктурные библиотеки (ORM, драйверы БД)
  • пакеты с бинарными модулями

ssr.noExternal и обратная логика

Противоположный механизм — ssr.noExternal.

export default {
  ssr: {
    noExternal: ['some-esm-package']
  }
}

Он принудительно заставляет Vite включить пакет в SSR-бандл.

Зачем нужен noExternal

Некоторые пакеты:

  • поставляются как ESM-only
  • используют современные синтаксические конструкции
  • требуют трансформации через esbuild
  • ломаются при прямом Node-resolve

В таких случаях экстернализация приводит к runtime error:

  • Unexpected token export
  • Cannot use import statement outside a module

Типичный кейс

Пакет написан под ESM, но Node.js проект использует смешанный режим:

  • SSR требует транспиляции
  • зависимость не совместима с require()

Тогда noExternal заставляет Vite обработать пакет как часть графа сборки.

Авто-экстернализация и heuristics Vite

Vite применяет эвристики:

1. node_modules по умолчанию external

Большинство зависимостей не бандлятся в SSR, если не требуется трансформация.

2. Исключения для ESM трансформации

Если пакет:

  • содержит ESM синтаксис
  • требует pre-bundling
  • импортируется через deep imports

Vite может включить его в бандл.

3. Разделение client/server графов

Vite строит два графа:

  • client bundle (браузер)
  • server bundle (Node SSR)

И решения по external принимаются отдельно.

Конфликты между external и dependency optimization

Vite имеет отдельный механизм dependency pre-bundling (optimizeDeps), который относится к dev-клиенту. SSR external работает независимо, но конфликты возникают:

Пример конфликта

  • dependency оптимизирована через esbuild для client
  • SSR пытается external
  • пакет имеет разные entrypoints

Результат:

  • различия в резолве модулей
  • hydration mismatch
  • runtime ESM/CJS ошибки

Node built-ins и автоматическая экстернализация

В SSR автоматически external становятся встроенные модули Node.js:

  • fs
  • path
  • crypto
  • stream

Это поведение важно для предотвращения их попадания в бандл. Их бандлинг невозможен или бессмысленен, так как они предоставляются runtime.

Паттерны настройки SSR external

1. Строгая экстернализация инфраструктурных зависимостей

Используется в backend-heavy SSR:

ssr: {
  external: [
    'pg',
    'mysql2',
    'redis',
    'mongoose'
  ]
}

Цель — оставить тяжёлые зависимости вне бандла.

2. Частичная инверсия через noExternal

ssr: {
  noExternal: [
    '@scope/ui-components'
  ]
}

Применяется для UI библиотек, которые:

  • используют JSX трансформации
  • зависят от Vite pipeline
  • не совместимы с Node ESM resolution

3. Гибридная модель

Комбинация:

  • external для инфраструктуры
  • noExternal для ESM-UI пакетов
  • default behavior для остального

Глубокие проблемы экстернализации

1. Разные entrypoints пакета

Некоторые пакеты имеют:

  • browser build
  • node build
  • module/require split

Vite может выбрать не тот entrypoint при external, если package.json exports сложный.

2. Dual package hazard

Пакеты с CJS + ESM могут приводить к:

  • дублированию инстансов
  • различию singleton state
  • несогласованным импортам

3. Hoisting и монорепозитории

В pnpm/yarn workspaces:

  • зависимости могут резолвиться выше
  • SSR external может “потерять” пакет
  • возникает module not found

SSR external и производительность

Экстернализация напрямую влияет на:

  • время сборки SSR
  • размер server bundle
  • скорость cold start
  • память процесса Node.js

Бандлинг уменьшает число файлов, но увеличивает CPU cost на сборку. External снижает CPU, но увеличивает runtime resolution cost.

Баланс зависит от:

  • частоты SSR рендеринга
  • размера приложения
  • количества зависимостей

Практика резолва и диагностика

Типовые ошибки:

  • Cannot find module
  • Named export not found
  • Unexpected token ‘export’
  • require() of ES Module

Диагностика обычно начинается с анализа:

  • SSR graph Vite
  • transformed server bundle
  • package.json exports field

Особенно критичны пакеты с:

  • conditional exports
  • node/ browser field splitting
  • mixed module systems

Итоговая модель поведения

Экстернализация в SSR Vite можно рассматривать как трёхслойную систему:

  • автоматический external для node_modules
  • ручной control через ssr.external
  • принудительный инлайнинг через ssr.noExternal

Она работает поверх Node.js resolution, сохраняя баланс между:

  • скоростью сборки
  • корректностью исполнения
  • совместимостью модулей
  • управляемостью dependency graph