Настройка ssr.noExternal и ssr.external

SSR в Vite строится вокруг принципа разделения зависимостей на те, что исполняются в Node.js как внешние модули, и те, что должны быть включены в серверный бандл через esbuild/Rollup. Управление этим поведением осуществляется через две ключевые настройки: ssr.external и ssr.noExternal. Эти параметры напрямую влияют на то, как Vite обрабатывает node_modules при серверной сборке, как разрешаются импорты и какие пакеты оказываются в итоговом SSR-бандле.


При сборке SSR-приложения Vite не стремится повторить поведение браузерного бандлера. Вместо этого он формирует Node-ориентированную графовую структуру модулей, где каждый импорт классифицируется как:

  • external (внешний) — остаётся require()/import в Node.js
  • bundled (встроенный) — включается в SSR-бандл

По умолчанию Vite пытается:

  • оставлять большинство зависимостей внешними
  • бандлить только код приложения и ESM-зависимости, которые требуют трансформации

Это поведение можно переопределить двумя опциями.


ssr.external: явное исключение из бандла

Назначение

ssr.external определяет список зависимостей, которые никогда не будут включены в SSR-бандл и всегда останутся внешними импортами.

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

Поведение

Если модуль попадает в ssr.external, Vite:

  • не обрабатывает его через esbuild
  • не включает его в серверный бандл
  • оставляет import как есть
  • предполагает, что Node.js сможет его загрузить самостоятельно

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

1. Node-native библиотеки

Некоторые библиотеки рассчитаны исключительно на Node.js и не должны бандлиться:

  • fs, path, crypto (встроенные модули Node)
  • нативные модули (bcrypt, sharp, sqlite3)

Пример:

ssr: {
  external: ['sharp', 'bcrypt']
}

2. Крупные зависимости с собственным резолвом

Некоторые пакеты сами управляют загрузкой подмодулей:

  • ORM (Prisma, Sequelize)
  • фреймворки (NestJS модули)
  • драйверы БД

Бандлинг таких пакетов может ломать динамические импорты.


3. Монорепозитории с hoisting

В pnpm/yarn workspaces зависимости могут находиться вне node_modules текущего пакета. Пометка их как external снижает риск двойного резолва.


Важные особенности ssr.external

Не влияет на клиентскую сборку

ssr.external работает только в SSR-контексте. Браузерный бандл не затрагивается.


Поддержка паттернов

Можно использовать строки или паттерны:

ssr: {
  external: [
    'lodash',
    /^@nestjs\//,
    'pg'
  ]
}

Регулярные выражения позволяют исключать целые namespace-пакеты.


Потенциальная проблема

Если пометить пакет как external, но он использует ESM-экспорт, Node может потребовать дополнительной конфигурации "type": "module" или interop-обёртки. Это часто проявляется ошибками:

  • ERR_REQUIRE_ESM
  • Cannot use import statement outside a module

ssr.noExternal: принудительное включение в бандл

Назначение

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

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

Как работает ssr.noExternal

При обработке графа модулей Vite:

  1. проверяет импорт
  2. если пакет совпадает с noExternal
  3. принудительно прогоняет его через esbuild
  4. включает в SSR-бандл

Когда использовать ssr.noExternal

1. ESM-библиотеки, плохо работающие в Node

Некоторые пакеты распространяются как ESM-only и не дружат с CommonJS окружением Node SSR:

  • современные UI-библиотеки
  • утилиты с export default без CJS fallback

Пример:

ssr: {
  noExternal: ['unified', 'remark']
}

2. Пакеты с динамическими импортами

Если библиотека использует import() внутри и Node резолвит их некорректно, бандлинг устраняет проблему.


3. Пакеты с зависимостями на браузерные API

Некоторые библиотеки содержат условный код:

if (typeof window !== 'undefined')

При SSR это может ломать выполнение. Бандлинг позволяет Vite провести tree-shaking и заменить окружение.


Поведение при конфликте ssr.external и ssr.noExternal

Если один и тот же пакет попадает в оба списка:

  • приоритет у ssr.noExternal
  • пакет будет включён в бандл

Гранулярность применения

Строки

ssr: {
  noExternal: ['vue', 'vue-router']
}

Регулярные выражения

ssr: {
  noExternal: [/^@vue\//]
}

Смешанный вариант

ssr: {
  external: ['lodash'],
  noExternal: ['vue']
}

Взаимодействие с node_modules резолвом

Vite SSR использует Node resolution алгоритм, но с дополнительным анализом:

  • проверка ESM/CJS формата
  • анализ package.json (type, exports)
  • выявление побочных эффектов (sideEffects)

noExternal заставляет Vite игнорировать часть этой логики и всегда идти через бандлер.


Типовые сценарии настройки


1. Стандартный SSR-проект (Vue/React)

Обычно:

  • большинство UI-библиотек → noExternal
  • серверные утилиты → external
ssr: {
  noExternal: ['vue', 'vue-router', 'pinia']
}

2. Node-heavy backend SSR

Если SSR используется как серверный рендеринг API + HTML:

ssr: {
  external: [
    'fs',
    'path',
    'crypto',
    'pg',
    'mysql2'
  ]
}

3. Монорепозиторий

При workspace структуре:

ssr: {
  noExternal: [
    /^@shared\//,
    /^@ui\//
  ]
}

Это гарантирует, что внутренние пакеты будут правильно транспилироваться.


Частые проблемы и их причины

1. ERR_REQUIRE_ESM

Причина:

  • CJS пытается загрузить ESM пакет

Решение:

  • добавить пакет в noExternal

2. Дублирование зависимостей

Причина:

  • пакет бандлится, но также подтягивается как external зависимость

Решение:

  • явно зафиксировать в external или noExternal

3. Ломается tree-shaking

Причина:

  • принудительный external не даёт бандлеру оптимизировать код

Решение:

  • перевести пакет в noExternal

Влияние на производительность

ssr.external

Плюсы:

  • меньше размер бандла
  • быстрее сборка

Минусы:

  • больше runtime-зависимостей
  • возможные ошибки резолва в Node

ssr.noExternal

Плюсы:

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

Минусы:

  • больше время сборки
  • увеличенный SSR bundle size

Практическая стратегия настройки

На практике используется гибридный подход:

  • external: всё, что нативно для Node
  • noExternal: всё, что ESM-first или нестабильно в SSR

Типичная базовая конфигурация:

export default {
  ssr: {
    noExternal: [
      'vue',
      'vue-router',
      'pinia'
    ],
    external: [
      'sharp',
      'bcrypt',
      'pg'
    ]
  }
}

Ментальная модель

ssr.external и ssr.noExternal можно рассматривать как два слоя контроля:

  • external — «оставить Node разбираться самому»
  • noExternal — «обязательно обработать через Vite»

Их комбинация определяет границу между:

  • runtime resolution (Node.js)
  • build-time bundling (Vite + esbuild)

Резюме поведения резолва

При импорте модуля в SSR:

  1. Проверяется ssr.noExternal
  2. Проверяется ssr.external
  3. Применяется дефолтная эвристика Vite
  4. Модуль либо бандлится, либо остаётся external

Эта последовательность критична для понимания неожиданных эффектов в SSR-сборках Vite.