# Vite SSR BOOST
> SSR and streaming for existing Vite + React Router apps in Data mode, without Framework mode
Package: `@lomray/vite-ssr-boost`
Version: `8.4.2`
## Public entrypoints
- Vite plugin: `@lomray/vite-ssr-boost/plugin`.
- Cloudflare Workers: `@lomray/vite-ssr-boost/cloudflare` (`createWorkerHandler`, `getHtmlFromAssets`, `RouteAssets`, `TRouteAssetsManifest`); see [Cloudflare Workers](https://lomray-software.github.io/vite-ssr-boost/guide/cloudflare).
- Browser entry: `@lomray/vite-ssr-boost/browser/entry`.
- Managed CLI server entry: `@lomray/vite-ssr-boost/adapters/express/entry`.
- Fetch core: default export `createHandler` from `@lomray/vite-ssr-boost/core/handler`.
- Transport adapters: `@lomray/vite-ssr-boost/adapters/node`, `adapters/express`, `adapters/fastify`, `adapters/hono` and `adapters/edge`, all under the package prefix.
- Renderers: `@lomray/vite-ssr-boost/node/render-to-stream` and `@lomray/vite-ssr-boost/edge/render-to-stream`.
- SSR route testing: `@lomray/vite-ssr-boost/testing`; optional browser assertions: `@lomray/vite-ssr-boost/testing/playwright`. See [Testing](https://lomray-software.github.io/vite-ssr-boost/guide/testing).
- Node production helpers: `@lomray/vite-ssr-boost/node/production`.
- CLI binary: `ssr-boost`.
## Data loading contract
Loader and action promises stream by default in Data mode. Return `{ fast, slow: fetchSlow() }` and consume `slow` inside Suspense with `` or React 19 `use()`. The browser reconstructs native promises before creating the router; client navigation loaders keep their native promises. See [Stream loader data](https://lomray-software.github.io/vite-ssr-boost/guide/data-streaming) for the supported value matrix, errors and opt-in `hydration: 'early'`. Custom `getState` snapshots still use JSON.
---
Source: https://lomray-software.github.io/vite-ssr-boost/ai-usage
# AI Usage
Use this page as a grounding reference for tools working with the package.
## AI agents
Two [Agent Skills](https://agentskills.io/specification) are available: `ssr-boost-migrate` for an existing Vite SPA and `ssr-boost-new-app` for a new application. They include entry/data references and verification scripts.
**Claude Code** — install the [plugin](https://code.claude.com/docs/en/plugins):
```sh
claude plugin marketplace add Lomray-Software/vite-ssr-boost
claude plugin install ssr-boost@lomray
```
Invoke `/ssr-boost:ssr-boost-migrate` or `/ssr-boost:ssr-boost-new-app`. The GitHub install uses the repository's default branch; it requires the skill files to be present there. To try a checkout before release, run `claude --plugin-dir /absolute/path/to/vite-ssr-boost`.
**Codex CLI** — from a clone of this repository, copy the complete folders (including references/scripts) into the personal skill directory:
```sh
mkdir -p ~/.codex/skills
cp -R skills/ssr-boost-migrate skills/ssr-boost-new-app ~/.codex/skills/
```
Or copy them into the target repository:
```sh
mkdir -p /path/to/app/.codex/skills
cp -R skills/ssr-boost-migrate skills/ssr-boost-new-app /path/to/app/.codex/skills/
```
The [current Codex documentation](https://developers.openai.com/codex/skills) specifies `.agents/skills` for discovery. For those versions, copy into `~/.agents/skills` or the application's `.agents/skills` instead, or expose the personal copies above with per-skill symlinks:
```sh
mkdir -p ~/.agents/skills
ln -s ~/.codex/skills/ssr-boost-migrate ~/.agents/skills/ssr-boost-migrate
ln -s ~/.codex/skills/ssr-boost-new-app ~/.agents/skills/ssr-boost-new-app
```
Use one discovered copy of each skill. For repository copies, use the equivalent symlinks from `.agents/skills/` to `../../.codex/skills/`. Invoke `$ssr-boost-migrate` or `$ssr-boost-new-app`; check `/skills` and restart Codex if the skills do not appear. Review existing folders before copying updates.
**Cursor** — copy the skill folders into the app's `skills/` directory, then add a [project rule](https://cursor.com/docs/context/rules) at `.cursor/rules/ssr-boost.mdc`:
```md
---
description: SSR Boost migration and new application workflows
alwaysApply: false
---
For a Vite SPA migration, read @skills/ssr-boost-migrate/SKILL.md.
For a new SSR Boost app, read @skills/ssr-boost-new-app/SKILL.md.
Follow the selected skill's references and verification procedure.
```
**Other agents** — start with [llms.txt](https://lomray-software.github.io/vite-ssr-boost/llms.txt), or use [llms-full.txt](https://lomray-software.github.io/vite-ssr-boost/llms-full.txt) for the documentation and both skill procedures. The [benchmark reference](/reference/benchmarks) points to current measurements without embedding result numbers.
## Package identity
`@lomray/vite-ssr-boost` adds SSR to React Router apps in Data mode, without moving to Framework mode and without rewriting the app. Keep the Vite configuration, route objects and components; use the same application for SSR or SPA output.
For [incremental SSR](/guide/incremental-ssr), managed Express entries and Fetch `createHandler` accept an `ssr` policy with include/exclude patterns and per-request decisions, default crawler SSR, and a restart-only `SSR_BOOST_SSR_ROUTES` rollback override.
Data mode, not Framework mode. The server uses `createStaticHandler` and `StaticRouterProvider`, and the browser uses route objects with `createBrowserRouter`; see React Router's [mode definitions](https://reactrouter.com/start/modes).
The package declares `engines.node: ">=22.12.0"` and peers for Vite `>=5`, React and React DOM `>=18.2.0`, and React Router `>=7.0.1`. The template tooling uses Node 22.23.2. Check the selected React Router and build tool versions for further engine requirements.
## Public entrypoints
- Vite plugin: `@lomray/vite-ssr-boost/plugin`.
- Cloudflare Workers: `@lomray/vite-ssr-boost/cloudflare` (`createWorkerHandler`, `getHtmlFromAssets`, `RouteAssets`, `TRouteAssetsManifest`); see [Cloudflare Workers](/guide/cloudflare).
- Browser entry: `@lomray/vite-ssr-boost/browser/entry`.
- Managed CLI server entry: `@lomray/vite-ssr-boost/adapters/express/entry`.
- Fetch core: default export `createHandler` from `@lomray/vite-ssr-boost/core/handler`.
- Transport adapters: `@lomray/vite-ssr-boost/adapters/node`, `adapters/express`, `adapters/fastify`, `adapters/hono` and `adapters/edge`, all under the package prefix.
- Renderers: `@lomray/vite-ssr-boost/node/render-to-stream` and `@lomray/vite-ssr-boost/edge/render-to-stream`.
- SSR route testing: `@lomray/vite-ssr-boost/testing`; optional browser assertions: `@lomray/vite-ssr-boost/testing/playwright`. See [Testing](/guide/testing).
- Node production helpers: `@lomray/vite-ssr-boost/node/production`.
- CLI binary: `ssr-boost`.
## Default workflow and transport ownership
Prefer the managed CLI for Vite development, HMR, asset manifests and production static files. It uses Express, and managed request/render hooks retain the live Express `req` and `res` objects. `node/entry` was removed in 8.0.0; use `adapters/express/entry` for v8 and follow [Upgrade from 7 to 8](/guide/upgrade-v8).
A Fetch transport owns its development server, bundling, static assets and route-asset injection. It can initialize `ServerConfig` from `@lomray/vite-ssr-boost/services/server-config` and inject manifest assets with `SsrManifest` from `@lomray/vite-ssr-boost/services/ssr-manifest`; see [Runtime adapters](/guide/runtime-adapters). The [custom-server example](https://github.com/Lomray-Software/vite-template/tree/example/custom-server) demonstrates this integration with Fastify in production and the managed CLI in development.
## Data loading contract
Loader and action promises stream by default in Data mode. Return `{ fast, slow: fetchSlow() }` and consume `slow` inside Suspense with `` or React 19 `use()`. The browser reconstructs native promises before creating the router; client navigation loaders keep their native promises. See [Stream loader data](/guide/data-streaming) for the supported value matrix, errors and opt-in `hydration: 'early'`. Custom `getState` snapshots still use JSON.
## Application guidance
For an existing Vite + React Router app, run `ssr-boost init --dry-run` first and review every diff before `--apply`. After changing entries, routes, plugins, scripts, or dependencies, run `ssr-boost doctor --json`; resolve errors and inspect version warnings before building. See the [CLI reference](/api/cli) for `--root`, overrides and support bundles.
- Keep lazy route imports statically analyzable.
- Align Vite `base` with the server static middleware basename.
- Use `onRequest` for app props scoped to a request.
- Use `onRouterReady` to choose streaming or a complete render.
- Use `getState` with `getServerState` to restore application state.
- Use `OnlyClient` for browser-only widgets and lazy `onlyClient` routes for pages.
- Read the [migration guide](/guide/migrate-existing-spa) for entries copied from the minimal example.
## Details to preserve
- The browser entry resolves matched lazy routes and waits for the SSR state before creating the router and hydrating.
- `data-force-spa="1"` on the root forces SPA mounting.
- Rendering and pending router promises abort on timeout or request cancellation.
- Early hydration requires an async client entry and custom state available at `onShellReady`; buffered bot rendering waits for all promises.
- CLI focus selection uses `--focus-only`; use `--focus-only client` for SPA build and start.
- Plugin `entrypoint` configures additional build surfaces.
- The package does not implement RSC, Server Actions or file-system routing conventions.
- Use the [comparison guide](/guide/choosing) and its official sources for claims about other projects.
## Machine-readable documentation
The docs build generates [llms.txt](https://lomray-software.github.io/vite-ssr-boost/llms.txt) and [llms-full.txt](https://lomray-software.github.io/vite-ssr-boost/llms-full.txt) from the Markdown sources. The short file includes the package identity, the resolved release version when available, the public entrypoints and data-loading contract from this page, and an absolute URL for every documentation page. The full file concatenates those pages and both `skills/*/SKILL.md` procedures with their source URLs; relative skill links become absolute repository links.
Run `npm run docs:build` to regenerate both files in `docs/.vitepress/dist`. The version comes from `git describe --tags --match 'v*' --abbrev=0`, with the leading `v` removed. If tags are unavailable, the build falls back to `npm view @lomray/vite-ssr-boost version`; if that also fails, it omits the version line. The build reports which source it used. The docs workflow fetches full Git history and tags so CI can use the tag path. The source manifest's placeholder version is never used; semantic-release assigns the published package version in `lib/package.json` separately.
Generation checks that every indexed URL has a built HTML page and reports the page/link counts. GitHub Pages deploys these files with the rest of the docs; generated copies are not checked in.
## Keeping the Context7 index fresh
[`context7.json`](https://github.com/Lomray-Software/vite-ssr-boost/blob/prod/context7.json) selects the `prod` branch and `docs/`. Context7 also includes root Markdown automatically; the filename exclusions leave `README.md` as the root source. Tests, scripts, package output and non-documentation examples are excluded, while `docs/examples/` stays indexed. The rules identify the v8 entrypoints and the removed `node/entry` path. The `v7.1.0` tag is configured as a separate previous version for v7 users. See the [Context7 configuration guide](https://context7.com/docs/library-owners).
Maintainers set the optional repository Actions secret `CONTEXT7_API_KEY`. The [refresh workflow](https://github.com/Lomray-Software/vite-ssr-boost/blob/prod/.github/workflows/context7-refresh.yml) calls the [documented refresh API](https://context7.com/docs/integrations/github-actions) on published releases or manual dispatch. The `prod` release job also calls it as a reusable workflow: [events created with `GITHUB_TOKEN` do not start another workflow](https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/trigger-a-workflow). This also refreshes docs-only changes after a successful `prod` release job.
The API step uses a job environment variable in its condition, following [GitHub's optional-secret pattern](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets), so a missing key skips the request. The request refreshes the library's configured source; `context7.json` selects `prod`, so publishing a prerelease does not switch the main index to `staging`. After promoting documentation to `prod`, maintainers can dispatch the workflow to retry a failed refresh or refresh without a release. Check the workflow result and the [Context7 listing](https://context7.com/lomray-software/vite-ssr-boost) after indexing completes; a successful API request queues refresh work and does not prove parsing has finished.
---
Source: https://lomray-software.github.io/vite-ssr-boost/api/browser-entry
# Browser Entry
## Import
```ts
import entryClient from '@lomray/vite-ssr-boost/browser/entry';
```
## Signature
```ts
entryClient(App, routes, options?)
```
Options:
```ts
interface IEntryClientOptions {
init?: (params: {
isSSRMode: boolean;
router: DataRouter;
}) => Promise;
routerOptions?: Parameters[1];
createRouter?: typeof createBrowserRouter;
rootId?: string;
}
```
## What it does
While the document is loading, the entry waits for serialized router state (SSR only) or `DOMContentLoaded` before creating the router and rendering.
The browser entry:
- resolves currently matched lazy routes before router creation
- creates the browser router
- runs optional async initialization
- either hydrates SSR HTML or mounts a pure SPA root
That lazy-route preload step is important. It keeps SSR hydration from diverging when a route module was lazy on the server but not yet loaded on the client.
## `init`
Use `init` to create client-only props for your top-level app wrapper.
```tsx
void entryClient(App, routes, {
init: async ({ isSSRMode, router }) => ({
isSSRMode,
pathname: router.state.location.pathname,
}),
});
```
Those values are passed to `App` as `client`.
`isSSRMode` reflects the current document: it is `false` for an [incremental SSR](/guide/incremental-ssr) SPA shell even when the shared browser bundle was built for SSR.
## `createRouter`
The default is `createBrowserRouter`.
Override it when your app uses a wrapped router factory from another integration, such as monitoring or instrumentation tooling.
## `rootId`
Defaults to `root`.
Change it only if your HTML template uses a different root node id.
---
Source: https://lomray-software.github.io/vite-ssr-boost/api/cli
# CLI
## Binary
```bash
ssr-boost
```
The CLI is the operational face of the package. It drives development, builds, preview mode and deployment-oriented packaging.
## Main commands
### `ssr-boost init`
Adds SSR to an existing Vite + React Router Data-mode app. The default is a dry run:
```bash
npx ssr-boost init --dry-run
npx ssr-boost init --apply
npm install
```
Before installing the library, invoke it with `npx --package @lomray/vite-ssr-boost ssr-boost init`.
| Option | Meaning |
| --- | --- |
| `--dry-run` | Print a unified diff of every proposed file; write nothing (default). |
| `--apply` | Write the complete plan after validating it. Cannot be combined with `--dry-run`. |
| `--root ` | Project directory; defaults to the current directory. |
| `--entry ` | Browser file relative to the project; must match the HTML module script. |
| `--routes ` | Module with one exported route array, relative to the project. |
Example output excerpt:
```diff
--- a/index.html
+++ b/index.html
@@ -1 +1 @@
-
+
```
```text
Dry run: no files written. Use --apply to write these changes.
Next: npm install; then npx ssr-boost doctor.
```
The remaining diffs show the Vite plugin, browser/server entries, scripts and dependency, plus an extracted route module when the routes were inline. The CLI preserves surrounding source, quotes and line endings where possible; it does not require Prettier. It never installs dependencies. A second `--apply` prints `No changes: SSR is already initialized. Run ssr-boost doctor.`
An existing `dev` script becomes `ssr-boost dev`; no duplicate `develop` is added. Without `dev`, the command updates or adds `develop`. It always sets `build` to `ssr-boost build` and adds `start:ssr` as `ssr-boost start`; a stock `vite preview` script becomes `ssr-boost preview`. Existing routes imports keep their module specifier and binding (for example, `import { routes } from './routes'`). `StrictMode`, Fragment, and unwrapped roots get an explicit App component in both entries to preserve the render tree without forwarding entry props to React built-ins.
Unsupported layouts exit 1 before writing and include the [manual migration guide](/guide/migrate-existing-spa). See that guide for supported layouts and constraints, including custom bases and provider initialization.
### `ssr-boost doctor`
Inspects the project without importing application modules, executing Vite plugins, or reading environment files:
```bash
npx ssr-boost doctor
npx ssr-boost doctor --json --root ./my-app
npx ssr-boost doctor --bundle support.json
```
| Option | Meaning |
| --- | --- |
| `--json` | Print a JSON report instead of the check table. |
| `--root ` | Project directory; defaults to the current directory. |
| `--bundle ` | Write a support bundle JSON, relative to the project directory (absolute paths also work). |
Each table/JSON check has `name`, `status` (`ok`, `warn`, `error`), a message and a one-line `fix`. Exit status is **1 if any check is an error**, otherwise **0**. An `ok` fix describes how to maintain that condition.
Table excerpt:
```text
status check message fix
ok node Node 22.23.2 satisfies engines Use a Node version satisfying the package and app engines (CI uses 22.23.2).
ok vite-plugin SsrBoost() is in the Vite plugins Add SsrBoost() from @lomray/vite-ssr-boost/plugin to Vite plugins.
error html-outlet index.html: expected one outlet, found 0 Place exactly one inside the root element.
```
JSON check excerpt:
```json
{
"name": "routes",
"status": "ok",
"message": "2 route IDs resolved",
"fix": "Use statically analyzable route objects; follow the file and line in the parser error."
}
```
Checks cover readable package metadata; installed `@lomray/vite-ssr-boost`, React, React DOM, React Router and Vite versions; Node against the library, app and installed runtime engines; physical duplicate React copies via `npm ls react react-dom --json --all --long`; the imported Vite plugin; the HTML module script and exactly one outlet; both library entries; the actual route parser; and development/build/SSR scripts. Per-package `version:*` checks verify installation and peer ranges; missing packages or unsatisfied peer ranges are errors. A single `compatibility` check is `ok` only when React and React DOM, React Router, and Vite exactly match a row in the [compatibility workflow](https://github.com/Lomray-Software/vite-ssr-boost/blob/staging/.github/workflows/react-compatibility.yml). Otherwise it warns with the installed trio and closest tested row. The closest row has the most matching components, with ties resolved by proximity of React, Router, then Vite versions. Those rows ship with the CLI and a test prevents drift.
```text
warn compatibility React 19.2.8 + Router 7.18.3 + Vite 8.2.2 is not a tested row; closest: React 19.2.8 + Router 7.18.3 + Vite 7.3.6
fix: Use React and React DOM 19.2.8, React Router 7.18.3, and Vite 7.3.6 for a tested combination.
```
The `robots` and `size-budget` checks have `info: true`: they report missing/custom/blocked crawl policies and the presence of a size budget script without changing policy or failing the check. Doctor uses static configuration analysis; configuration it cannot resolve gets an actionable error instead of executing arbitrary config code.
The support bundle (`schemaVersion: 1`) includes versions, adapter, route IDs and literal paths, check names/statuses/fixes, and known diagnostic codes from the last recorded build. It omits raw error messages, npm trees, source code, cookies, environment values, loader/state data, and other application data. A pathless route or a dynamic path helper is represented by `path: null`; the tool does not execute path helpers. Successful managed builds record only known codes in `/ssr-boost-diagnostics.json`, resetting the list for each build. An absent or invalid record yields an empty list. See [development diagnostics](/reference/diagnostics) for code meanings.
### `ssr-boost dev`
Runs the development server.
Common flags:
- `--host`
- `--port`
- `--reset-cache`
- `--mode`
- `--entrypoint`
## `ssr-boost build`
Creates a production build.
Common flags:
- `--focus-only [all|app|client|server|entrypoint]`
- `--mode`
- `--client-options`
- `--server-options`
- `--unlock-robots`
- `--eject`
- `--serverless`
- `--throw-warnings`
## `ssr-boost start`
Runs the production server.
Common flags:
- `--host`
- `--port`
- `--focus-only`
- `--build-dir`
- `--module-preload`
## `ssr-boost preview`
Builds in watch mode and boots the production server once output is ready.
Common flags:
- `--focus-only`
- `--host`
- `--port`
- `--mode`
- `--build-dir`
## Deployment helpers
### `ssr-boost build-docker`
Flags:
- `--image-name`
- `--docker-options`
- `--docker-file`
- `--focus-only`
- `--mode`
### `ssr-boost build-amplify`
Flags:
- `--manifest-file`
- `--is-optimize`
- `--mode`
### `ssr-boost build-vercel`
Flags:
- `--config-file`
- `--config-vc-file`
- `--is-optimize`
- `--mode`
## Focus modes
`--focus-only` controls which part of the app should be built or started:
- `all`
- `app`
- `client`
- `server`
- `entrypoint`
This is the main switch when you want a narrower production action instead of rebuilding everything every time.
---
Source: https://lomray-software.github.io/vite-ssr-boost/api/components-and-helpers
# Components And Helpers
## Components
### `Navigate`
```tsx
import Navigate from '@lomray/vite-ssr-boost/components/navigate';
```
Works like React Router navigation on the client, but on the server it writes a `Response` with `Location` and status.
Useful props:
- normal `NavigateProps`
- `status?: number` with default `301`
### `ResponseStatus`
```tsx
import ResponseStatus from '@lomray/vite-ssr-boost/components/response-status';
```
Sets the server response status from inside the route tree.
Example:
```tsx
```
### `ScrollToTop`
```tsx
import ScrollToTop from '@lomray/vite-ssr-boost/components/scroll-to-top';
```
Scrolls to top on pathname changes.
Prop:
- `shouldReloadReset?: boolean`
### `OnlyClient`
```tsx
import OnlyClient from '@lomray/vite-ssr-boost/components/only-client';
```
Loads and renders a component only on the client.
Props:
```ts
{
load: () => Promise<{ default: ComponentType } | ComponentType>;
children: (Component: ComponentType) => ReactNode;
fallback?: ReactNode;
errorComponent?: ReactNode;
isMemorized?: boolean;
}
```
Example:
```tsx
import('./heavy-chart')}>
{(Chart) => }
```
### `withSuspense`
```tsx
import withSuspense from '@lomray/vite-ssr-boost/components/with-suspense';
```
Wraps a component with a provided suspense boundary component and hoists non-React statics from the original component.
## Helpers
### `getServerState`
```ts
import getServerState from '@lomray/vite-ssr-boost/helpers/get-server-state';
```
Reads serialized server state written during SSR and exposed to the client.
Use it when `getState` in the server entry injects data that the browser should pick up during boot.
## Interfaces
Useful route-related imports:
```ts
import type { FCRoute, FCCRoute } from '@lomray/vite-ssr-boost/interfaces/fc-route';
import type { TRouteObject } from '@lomray/vite-ssr-boost/interfaces/route-object';
```
Use these when you want typed route components or route object declarations aligned with the package conventions.
---
Source: https://lomray-software.github.io/vite-ssr-boost/api/node-production
# Node production helpers
Use these helpers when your application owns its Node HTTP server and serves a build produced by
the SSR BOOST CLI. They supply the `getHtml` and `prepare` options for `core/handler`.
```ts
import {
createRouteAssetPreparer,
loadHtmlShell,
} from '@lomray/vite-ssr-boost/node/production';
import type {
IHtmlShell,
ILoadHtmlShellOptions,
IRouteAssetPreparerOptions,
TRouteAssetsManifest,
} from '@lomray/vite-ssr-boost/node/production';
```
The explicit `.js` import path also works. These helpers use Node's filesystem APIs.
## `loadHtmlShell`
```ts
interface ILoadHtmlShellOptions {
indexFile: string;
outlet?: string; // Default: ''
}
function loadHtmlShell(options: ILoadHtmlShellOptions): Promise<() => IHtmlShell>;
interface IHtmlShell {
header: string;
footer: string;
}
```
Reads the UTF-8 file once and splits it at the outlet. The file must contain exactly one outlet;
an empty outlet, missing outlet or repeated outlet throws an error naming the file. Filesystem
read errors propagate to the caller.
The returned function creates a new `{ header, footer }` object each time, so request hooks can
change the shell without affecting other requests. It can be passed directly as `getHtml`.
Load a new shell when deploying a new build.
## `createRouteAssetPreparer`
```ts
type IRouteAssetPreparerOptions = (
| { buildDir: string; manifest?: never }
| { manifest: TRouteAssetsManifest; buildDir?: never }
) & { modulePreload?: boolean }; // Default: false
function createRouteAssetPreparer(
options: IRouteAssetPreparerOptions,
): NonNullable['prepare']>;
```
`ICreateHandlerOptions` is the type exported by `core/handler`.
Pass the build directory containing `client/` and `server/`. The preparer resolves this path when
created, then reads `server/assets-manifest.json` when a request first has matched routes. Each
preparer caches its own manifest and shares the managed server's asset injection logic. It does
not discover builds from the working directory or use the managed server's manifest singleton.
Relative paths resolve against the working directory at creation; use an absolute path to start
the application from any directory.
Alternatively, import the parsed **`build/client/assets-manifest.json`** and pass `{ manifest }`:
```ts
import manifest from './build/client/assets-manifest.json';
const prepare = createRouteAssetPreparer({ manifest });
```
The CLI emits this alongside the identical `build/server/assets-manifest.json` after the normal
server build. It is not Vite's `.vite/manifest.json`. The exported `TRouteAssetsManifest` type accepts
a JSON import directly. `RouteAssets` from `services/route-assets` also accepts the object as its
first constructor argument, with `modulePreload` as the second. Manifest objects are not mutated.
For Workers use the edge-safe [Cloudflare entry](/guide/cloudflare), which uses the same injection
logic without importing the Node helpers.
The hook runs after React Router matches the request. It inserts matching route styles and scripts
before `` in `context.html.header`; the shell must contain that closing tag. Lazy route IDs
must match the routes used for the build. `modulePreload: true` also inserts module preload links.
Asset URLs come from the manifest, including any base path configured during the build.
When the assets produce `Link` headers, the hook awaits `executionContext.onEarlyHints` if provided.
It works without that callback. The Node and Fastify adapters send these headers as HTTP 103 Early
Hints when the transport supports them. They are separate from the final response headers.
A missing manifest or a route without assets adds nothing, matching the managed server. Invalid
manifest JSON and read errors propagate. Create a new preparer when deploying a new build.
See the [Fastify launcher](/guide/runtime-adapters#adapters) for the complete server setup,
including static files and compression.
---
Source: https://lomray-software.github.io/vite-ssr-boost/api/plugin
# Plugin
## Import
```ts
import SsrBoost from '@lomray/vite-ssr-boost/plugin';
```
## Minimal usage
```ts
plugins: [SsrBoost(), react()];
```
## Options
```ts
interface IPluginOptions {
indexFile?: string;
serverFile?: string;
clientFile?: string;
routesPath?: string;
spaIndex?: boolean | {
filename?: string;
rootId?: string;
};
tsconfigAliases?: boolean | {
root?: string;
tsconfig?: string;
};
customShortcuts?: {
key: string;
description: string;
action: (cliContext) => Promise | void;
isOnlyDev?: boolean;
}[];
entrypoint?: IBuildEntrypoint[];
}
```
## Defaults
```ts
{
indexFile: 'index.html',
serverFile: 'server.ts',
clientFile: 'client.ts',
tsconfigAliases: true,
spaIndex: false,
}
```
## What the plugin does
At a high level it:
- sets `__IS_SSR__`
- prepares SSR build config behavior
- enables tsconfig alias import support
- can generate an extra SPA index
- can swap client entrypoints for custom builds
- normalizes route handling for manifest generation
## `spaIndex`
Set `spaIndex: true` to emit `index-spa.html`.
Custom form:
```ts
SsrBoost({
spaIndex: {
filename: 'index-spa.html',
rootId: 'root',
},
});
```
The generated file marks the root with `data-force-spa="1"` so the browser entry mounts instead of hydrating.
## `tsconfigAliases`
Enabled by default.
Use `false` if you do not want aliases from tsconfig copied into Vite config. Use the object form if your alias resolution needs custom parameters from the alias plugin.
## `entrypoint`
`entrypoint` defines additional app surfaces for build time.
```ts
interface IBuildEntrypoint {
name: string;
type: 'spa' | 'ssr';
indexFile?: string;
clientFile?: string;
serverFile?: string;
buildOptions?: string;
}
```
Example:
```ts
SsrBoost({
entrypoint: [
{
name: 'mobile',
type: 'spa',
clientFile: './src/mobile.tsx',
buildOptions: '--mode mobile',
},
],
});
```
## `customShortcuts`
Lets you extend interactive CLI keyboard actions in development.
Use this when your team has dev-only restart, cache or environment actions that should be one keypress away from the running dev process.
---
Source: https://lomray-software.github.io/vite-ssr-boost/api/server-entry
# Server Entry
## Import
```ts
import entryServer from '@lomray/vite-ssr-boost/adapters/express/entry';
```
## Signature
```ts
entryServer(App, routes, options?)
```
Options:
```ts
interface IEntryServerOptions {
ssr?: ISsrPolicy;
abortDelay?: number;
init?: (params: { config: ServerConfig }) =>
IEntrypointOptions | Promise>;
loggerProd?: Logger;
loggerDev?: Logger;
middlewares?: {
compression?: CompressionOptions | false;
expressStatic?: (ServeStaticOptions & { basename?: string }) | false;
};
routerOptions?: Parameters[1];
}
```
## What it returns
The entry returns a render definition consumed by the runtime server. It includes:
- `render`
- `init`
- `routes`
- `abortDelay`
- optional loggers
- optional middleware config
## `ssr`
Select SSR or the SPA shell per incoming URL, before route loaders run:
```ts
interface ISsrPolicy {
mode?: 'all' | 'include' | 'exclude';
routes?: (string | RegExp)[];
bots?: 'ssr' | 'policy';
decide?: (params: { request: Request; url: URL; isBot: boolean }) =>
'ssr' | 'spa' | undefined;
}
entryServer(App, routes, {
ssr: { mode: 'include', routes: ['/', '/articles/:slug'] },
});
```
`all` is the default. `include` uses SSR only for matching URL pathnames; `exclude` serves matches as SPA. Strings use path-to-regexp 8 syntax, including named wildcards and brace optional groups; RegExp patterns use their own flags. Include the router basename in patterns. `decide` overrides the configured mode per request, with `undefined` falling back to the route policy. `bots: 'ssr'` defaults to forcing detected crawlers to SSR ahead of all other decisions; use `'policy'` to opt out.
At entry creation, `SSR_BOOST_SSR_ROUTES` overrides `mode`, `routes` and `decide`, preserving `bots`. Comma-separated positive patterns form an include list; `!` patterns exclude URLs and win over includes. With only exclusions, other URLs stay SSR. An empty value selects SSR everywhere. Restart after changes; no rebuild is needed.
SPA responses retain `onRequest` and route asset preparation, use status 200 and the `data-force-spa` mount marker, and omit router/custom state and SSR render hooks. Active policies default documents to `Cache-Control: no-store` unless `onRequest` supplies a cache policy. The same `ssr` option is available on Fetch `createHandler`. See [Incremental SSR](/guide/incremental-ssr) for the full reference, lifecycle trade-offs and rollback recipe.
## Request lifecycle hooks
`init` resolves to a request lifecycle configuration:
```ts
interface IEntrypointOptions {
hydration?: 'footer' | 'early';
nonce?: string;
bootstrapScriptContent?: string;
onServerCreated?;
onServerStarted?;
onRequest?;
onRouterReady?;
onShellReady?;
onShellError?;
onResponse?;
onError?;
getState?;
}
```
See [Server Lifecycle](/guide/server-lifecycle) for the flow and intent of each hook.
## Hook context
`onRouterReady`, `onShellReady`, `onShellError`, `onError`, `onResponse` and `getState`
receive `{ context }`, with these fields:
- `request`: the Fetch `Request` built from the Express request; use it for headers, URL and method so hooks stay portable to other adapters.
- `response`: mutable Fetch `headers` and optional `status`; update these before the shell is sent.
- `req` / `res`: the live Express request and response, [deprecated in 8.x](/guide/upgrade-v8#deprecated-in-8-x) with removal planned for 9.0.
- `appProps`: request-scoped props returned by `onRequest`.
- `html`: the template `header` and `footer`.
- `routerContext` / `serverContext`: router and SSR metadata, once available.
- `isStream`, `hasEarlyHints` and `didError`: rendering mode, early-hints preference and error metadata.
`request` is the same object used by the Fetch core throughout a render; its `signal` tracks request cancellation.
```ts
onRouterReady: ({ context: { request } }) => ({
isStream: !request.headers.get('user-agent')?.includes('Googlebot'),
}),
```
## `onRequest`
The most important request hook.
It receives `(req, res)` before the render context is created.
It can return:
```ts
{
appProps?: TAppProps;
hasEarlyHints?: boolean;
shouldSkip?: boolean;
shouldCancel?: boolean;
}
```
That lets you shape app props, skip a request, or stop the rendering path entirely.
## `onResponse`
```ts
onResponse?: (params: {
context: IRequestContext;
html: string;
isEnd: boolean;
}) => string | undefined | void;
```
Regular HTML chunks arrive with `isEnd: false`. Returning `undefined` (or nothing) keeps the
original chunk; a string replaces it. Returning `''` withholds the chunk, allowing an
incremental transform to retain unfinished tokens.
After the composed body stream finishes, including the footer, the hook receives one final
call with `html: ''` and `isEnd: true`. Its returned string is appended to the response;
`undefined` or `''` appends nothing. This also applies to buffered rendering (`isStream: false`).
Hooks that ignore `isEnd` remain supported.
For example, using `@lomray/consistent-suspense`:
```ts
onResponse: ({ context: { appProps: { streamSuspense }, isStream }, html, isEnd }) => {
if (!isStream) return;
return isEnd ? streamSuspense.end() : streamSuspense.analyze(html);
},
```
## Middleware config
Production middleware can be configured without replacing the whole server pipeline:
```ts
export default entryServer(App, routes, {
middlewares: {
compression: {},
expressStatic: {
basename: '/static',
},
},
});
```
Set either option to `false` when you want it disabled.
## Logging
Use `loggerProd` and `loggerDev` to provide custom Vite-compatible loggers instead of the package default logger.
---
Source: https://lomray-software.github.io/vite-ssr-boost/api/testing
# Testing API
```ts
import {
createTestHandler, TestResponse, createDeferred, crawlerRequest, browserRequest,
} from '@lomray/vite-ssr-boost/testing';
```
See [Test SSR routes](/guide/testing) for complete Vitest and Playwright examples.
## createTestHandler(options)
Returns `{ fetch(input, init?): Promise }`. It renders in process through `createHandler` and `createStaticHandler`, without starting an HTTP server or following redirects. Relative paths resolve against `http://localhost`; absolute URLs and Fetch `Request` objects are accepted. The wrapper consumes the response stream immediately.
| Option | Description |
| --- | --- |
| `routes` | Required application route objects, including loaders, actions and lazy routes. |
| `App` | Optional React component receiving `children` and `server: context.appProps`. Without it, routes render directly. |
| `shell` | `{ indexFile, outlet? }` uses Node's cached `loadHtmlShell`; `{ header, footer }` works in any Fetch runtime. Omit for a minimal full HTML document and inert module-script placeholder. |
| `routerOptions` | Options forwarded to `createStaticHandler`, including `basename`. |
| `renderToStream` | Override the renderer with the core `TRenderToStream` contract. Defaults to Node pipeable streams or Web streams under edge export conditions. |
| `signal` | Default request cancellation signal, combined with the input Request and per-fetch signal. |
| `timeout` | Optional finite non-negative whole-request deadline in milliseconds. Includes routing, preparation and body reading. |
| Handler options | `diagnostics`, `hydration`, `abortDelay`, `onRequest`, `onRouterReady`, `onShellReady`, `onShellError`, `onError`, `onResponse`, `prepare`, `getState`, `nonce`, `bootstrapScriptContent`, `routerRequestContext` and `onContext` pass through to the Fetch core. |
`onContext({ context })` observes initialized request metadata before routing, including a bypass `onRequest` response (whose shell is empty). It can read `context.timeline?.events`. Each fetch has independent metadata and timeline; the route handler and cached HTML shell are shared by the kit instance.
`fetch(input, init?)` accepts `RequestInit` plus `isStream?: boolean` and `timeout?: number`. A supplied `isStream` overrides the result of `onRouterReady` for that request; the hook still runs. It never infers streaming from the user agent. Per-fetch timeout replaces the default timeout. Signals are combined, so either can cancel the request. Before headers, cancellation rejects fetch; after headers, body accessors reject with the signal reason. The `abortDelay` render deadline is separate and can finish a connected document with deferred rejection frames.
## TestResponse
You can also wrap an existing Fetch response with `new TestResponse(response)`. The `response` property exposes its original metadata; its body belongs to the wrapper's reader. All asynchronous accessors are repeatable.
| Member | Result |
| --- | --- |
| `status` | Numeric HTTP status. |
| `headers` | Fetch `Headers`. |
| `text()` / `html()` | `Promise` containing the full raw document. |
| `chunks()` | `Promise>`; UTF-8 is decoded as chunks arrive, and `at` is milliseconds since fetch began. Empty decoder fragments are omitted. |
| `routerState()` | `Promise` with `loaderData`, `actionData`, `errors`. Deferred fields become settled native values; rejected fields reject the accessor. Also reads legacy JSON hydration state. Missing or duplicate stream settlements throw; scripts are never executed. |
| `pendingBoundaries()` | `Promise` counting raw `` markers in final HTML. It does not execute React replacement scripts. |
| `streamFrames()` | `Promise` in document order. Tuples are `['init', payload, isEarly]`, `['resolve' \| 'reject', id, payload]` or `['shell']`; payloads retain their encoded strings. |
| `timeline()` | `Promise` after reading completes, including after an abort. Empty when recording is disabled or an independently wrapped Response has no supplied timeline. |
| `cookies()` | `Array<{ name: string; value: string; attributes: Record }>`; each Set-Cookie is separate. Attribute names are lowercase, flags are `true`, and values (including Expires and encoded cookie values) remain strings. |
`new TestResponse(response, { started?, timeline?, signal?, onComplete? })` also accepts a `performance.now()` request start, a core `RequestTimeline`, a cancellation signal, and a reader completion callback. These options are normally supplied by the kit.
## createDeferred<T>()
Returns `{ promise: Promise, resolve(value: T | PromiseLike): void, reject(reason?: unknown): void }`. Use it for loader/action fields that must settle after the shell. An attached rejection observer prevents unhandled-rejection noise while the router is still preparing; consumers still receive the original rejected promise.
## crawlerRequest(path) and browserRequest(path)
Return Fetch `Request` objects using the kit's default origin for relative paths. The former supplies a Googlebot user agent; the latter a browser user agent. They do not change render options.
## Playwright entry
```ts
import {
collectStreamTimeline, expectHydrated, expectStreamed,
} from '@lomray/vite-ssr-boost/testing/playwright';
```
The optional peer `@playwright/test >=1.40.0` is required when running these assertions. It is loaded only by this entry's assertion functions.
| Function | Contract |
| --- | --- |
| `collectStreamTimeline(page)` | Call and await before navigation. Returns `Promise<{ read(): Promise }>` and installs one observer for subsequent documents. |
| `expectHydrated(page, { root = '#root', timeout = 5000 } = {})` | Waits for the SSR Boost router event, document load, settled Suspense boundaries and browser rendering. Asserts state consumption, no observed React hydration errors, and no increased multiplicity of server text. `root` is a CSS selector for the root element. |
| `expectStreamed(page, selector)` | Waits for one matching visible element and asserts its first observed visibility is later than the shell. Uses Playwright's configured assertion timeout. |
Use the SSR Boost browser entry so router creation is observable. Call collection before `page.goto()`; installing it after hydration cannot reconstruct the original DOM or early console errors. The observer leaves stream delivery intact when the browser receiver replaces the queue's `push` method. Browser timeline offsets use navigation's `performance.now()` clock, not the server request clock.
## Timeline types
`ISsrRequestContext.timeline` is an optional request-local `RequestTimeline` with an `events` array. `ITimelineEvent` has `stage`, `at`, optional `id`, optional `placement: 'early' | 'footer'`, and optional `reason`. See the [stage table](/guide/testing#request-timeline) for semantics and the [diagnostics reference](/reference/diagnostics#ssr_boost_timeline) for environment configuration.
---
Source: https://lomray-software.github.io/vite-ssr-boost/examples/
# Example projects
The [vite-template repository](https://github.com/Lomray-Software/vite-template) contains the application examples. Start with `example/minimal` for the entries used in the [migration guide](/guide/migrate-existing-spa), or choose `prod` for state management and deployment workflows.
Use `npm create @lomray/ssr-app@latest my-app -- --template ` to create an app from a branch: `full` selects `prod`, `minimal` (the default) selects `example/minimal`, `custom-server` selects `example/custom-server`, and `localization` selects `example/localization`. See [Create a new app](/guide/getting-started#create-a-new-app) for the flags.
## `prod`
The [prod branch](https://github.com/Lomray-Software/vite-template/tree/prod) combines a MobX manager, consistent-suspense, a route manager and meta tags. It includes component-level data streaming, Docker, Amplify and Vercel build scripts, and deployment workflows. Use it when the application needs the state and stream wiring shown in its server and client entries.
## `example/minimal`
The [minimal branch](https://github.com/Lomray-Software/vite-template/tree/example/minimal) has six direct runtime dependencies: React, React DOM, React Router, vite-ssr-boost, `@lomray/react-head-manager` and `isbot`. It demonstrates loaders, a lazy route with CSS, redirects, 404 responses, metadata and a browser-only route. Its README includes the five-file change from a Vite SPA.
## `example/custom-server`
The [custom-server branch](https://github.com/Lomray-Software/vite-template/tree/example/custom-server) uses the managed CLI in development and an application-owned Fastify 5 launcher in production. Its `server/index.mjs` serves assets with `@fastify/static`, connects SSR through `adapterFastify(handler, { compression: true })`, injects route assets and emits Early Hints. Its `src/server.ts` exports the managed entry as the default and a `createHandler` handler as a named export, sharing the app between both servers. See [Runtime adapters](/guide/runtime-adapters) for the transport responsibilities.
## `example/localization`
The [localization branch](https://github.com/Lomray-Software/vite-template/tree/example/localization) creates an i18next instance per request and selects the language from the `lang` cookie, then `Accept-Language`. The server sets `` and transfers `{ language }` through `getState`; the client loads that language from bundled resources before hydration. A cookie-based switcher changes the language while keeping the server and browser in agreement.
## Data loading across examples
Follow the [data-loading contract](/guide/migrate-existing-spa#data-loading) for loader data at first paint and the Suspense pattern used for streamed data.
See [Recipes](/examples/recipes) for individual integrations.
---
Source: https://lomray-software.github.io/vite-ssr-boost/examples/recipes
# Recipes
## Packages that import CSS during SSR
If Node reports `Unknown file extension ".css"` from a dependency, let Vite bundle that package:
```ts
export default defineConfig({
ssr: { noExternal: ['the-package-that-imports-css'] },
});
```
## Keep server and browser state in sync
Create locale, authentication and store state per request. Serialize the initial values with
`getState`, then read them with `getServerState` before calling `entryClient`. Rendering a different
language or initial value in the browser causes hydration mismatches; log failures with `onError`.
## Change `basename`
Client:
```tsx
void entryClient(App, routes, {
routerOptions: {
basename: '/custom',
},
});
```
Server:
```tsx
export default entryServer(App, routes, {
routerOptions: {
basename: '/custom',
},
});
```
## Change static asset `base`
Vite config:
```ts
export default defineConfig({
base: '/static',
});
```
Server entry:
```tsx
export default entryServer(App, routes, {
middlewares: {
expressStatic: {
basename: '/static',
},
},
});
```
Keep these values aligned.
## Generate an extra SPA shell
```ts
export default defineConfig({
plugins: [
SsrBoost({
spaIndex: true,
}),
react(),
],
});
```
This emits `index-spa.html`, which is useful for service worker routing such as `createHandlerBoundToURL("index-spa.html")`.
## Add a Capacitor or mobile entrypoint
Vite config:
```ts
export default defineConfig({
plugins: [
SsrBoost({
entrypoint: [
{
name: 'mobile',
type: 'spa',
clientFile: './src/mobile.tsx',
buildOptions: '--mode mobile',
},
],
}),
react(),
],
});
```
Mobile entry:
```tsx
const AppMobile: FC = (props) => {
return ;
};
void entryClient(AppMobile, routes, {});
```
## Redirect with server status
```tsx
return ;
```
On the server this produces a redirect response instead of only changing client navigation state.
## Set HTTP status from a route
```tsx
const NotFound = () => (
<>
Page not found
>
);
```
## Load a widget only on the client
```tsx
import('./MapWidget')}
fallback={Loading map...
}
>
{(MapWidget) => }
```
Use this for browser-only integrations that should not participate in SSR.
---
Source: https://lomray-software.github.io/vite-ssr-boost/guide/caching
# Document headers and caching
Cache guest HTML at the CDN with a short freshness lifetime and a bounded
stale-while-revalidate window. Bypass **both cache reads and writes** whenever the
session cookie or Authorization header is present. Authenticated documents use
`private, no-store`; cache their underlying data with appropriate user or tenant keys
instead of sharing rendered pages.
Only opt URLs into a shared cache when their HTML, loader data and serialized custom
state are public and independent of unkeyed inputs. Put locale or other public variants
in the URL, or design a separate cache key. Login, account, cart and mutation routes
should bypass the page cache. Purge guest entries when content must disappear sooner
than their freshness plus stale window.
`private` excludes shared caches; `no-store` also excludes browser storage. Setting
Set-Cookie alone does not prohibit caching, so the document helper applies a privacy
default explicitly. See [RFC 9111](https://www.rfc-editor.org/rfc/rfc9111.html).
Stale-while-revalidate permits a bounded stale response while refresh runs in the
background; it does not extend freshness. See [RFC 5861](https://www.rfc-editor.org/rfc/rfc5861.html).
## HTTP helpers
Import the server-side `@lomray/vite-ssr-boost/http` entry. It uses Fetch primitives,
has no Node-only imports, and works with either renderer. Keep it out of browser entries.
All snippets on this page are compiled fixtures; the helper, Express, Worker and
Nginx examples also have executable tests.
### `cacheControl(policy)`
The boolean fields are `public`, `private`, `noStore`, `noCache`, `mustRevalidate`,
and `immutable`. The duration fields are `maxAge`, `sMaxAge`, `staleWhileRevalidate`,
and `staleIfError`, in non-negative safe integer seconds. False and undefined fields
are omitted; an empty object returns an empty string. Invalid types, unknown fields,
public plus private, and contradictory storage/revalidation directives throw TypeError.
These policies and ordered rules are shared by the following examples:
<<< ../../__fixtures__/caching/policy.ts
The SWR policy uses `max-age`, not `s-maxage`: RFC 9111 gives `s-maxage` the
semantics of proxy-revalidate, which prevents shared caches from serving stale
responses before validation. The `sharedPolicy` alternative gives browsers a zero
freshness lifetime and shared caches 30 seconds when stale serving is unnecessary.
The builder permits `sMaxAge` together with stale extensions, but does not change
the cache's interpretation. Cloudflare documents this restriction in its
[revalidation guide](https://developers.cloudflare.com/cache/concepts/revalidation/).
### `documentHeaders(rules, options?)`
The helper returns a function taking the Fetch render context and returning a fresh
Headers object. Each rule has `when({ request, url, isBot, hasCookie, routerContext })`
and `set: HeadersInit`. All matching rules run in array order. Later rules replace
ordinary fields; every Set-Cookie value appends independently. Existing hook headers
are the starting point. Guests without a matching policy keep those headers.
`isBot` is a User-Agent heuristic matching bot, crawler, spider or crawling; it is
not an authentication check. `hasCookie(name)` compares exact, case-sensitive cookie
names and treats empty values as present, without decoding the values.
Set `sessionCookie` to your application's cookie name. After the rules, Set-Cookie
on the resulting document, Authorization on the request, or that session cookie
forces `private, no-store`. Configure the same cookie name at the CDN. A deliberate
`protectPrivate: false` override disables this final protection; public responses
with Set-Cookie or the configured session cookie then trigger the development
[SSR_BOOST_CACHE_PRIVATE_LEAK diagnostic](/reference/diagnostics#ssr_boost_cache_private_leak).
The helper removes the Cookie token from Vary, preserving other tokens. **It never
emits Vary: Cookie**: varying on entire cookie strings fragments a CDN cache by
session IDs and unrelated tracking cookies. Use a session-presence bypass instead.
Do not use these URL-only recipes for pages personalized by other cookies; bypass
those pages too. Removing Vary does not make personalized content public.
Use the handler's `documentHeaders` option to apply the rules automatically after
`onShellReady` in `prepareHtmlResponse`, before document metadata is committed.
It is opt-in, including an empty rule list to enable only the privacy defaults.
Explicit loader/action and server redirects keep their existing `mergeResponseHeaders`
precedence; short-circuit `onRequest` responses and fatal shell errors are not rule
targets. Give those responses their own cache headers.
For manual use in `onRouterReady` or `onShellReady`:
<<< ../../__fixtures__/caching/manual.ts
The shell stage sees cookies written by earlier hooks. If you use the router stage,
recompute after any later header changes. Headers and statuses cannot change once
the streamed shell has been sent.
### `copyLoaderHeaders(routerContext, { allow })`
Rendered loader/action headers remain separate from document headers until explicitly
copied. This helper returns a new Headers object containing only the allowlist. It
visits matched routes from root to leaf, loader before action at the same route.
The first ordinary value wins, so the shallowest route controls Cache-Control and
other allowed fields. All Set-Cookie values append in traversal order, including
cookies with commas in Expires. Unmatched route headers are ignored. Content-Type
is never copied, even when allowed: loader JSON is not the HTML document.
The complete application factory below demonstrates copying without discarding
existing hook headers. Document rules run afterwards and override copied ordinary
fields. Supply the Node or edge renderer as shown in the recipes. This example renders
server HTML only; an interactive application supplies its built browser script and
static assets through its existing shell/asset pipeline. The cookie check is a
demonstration of presence, not session validation.
<<< ../../__fixtures__/caching/app.tsx
The managed CLI entry passes the same policy options through its `init` result:
<<< ../../__fixtures__/caching/managed.tsx
## Cloudflare Worker
This Worker caches `/guest` by the full URL, including the query string. Credentialed
requests bypass lookup and storage. Guest renders receive only the URL and a fixed
Accept header, so unkeyed cookies or headers cannot affect the shared representation.
HEAD, mutations, ranges, validators and explicit request cache directives go to the origin.
Only successful responses with the exact guest policy, no cookies and no Vary are stored.
The [Cache API](https://developers.cloudflare.com/workers/runtime-apis/cache/) does not
implement stale-while-revalidate. This wrapper stores the body for 90 seconds, returns
the original 30-second policy and Age to clients, and refreshes during the next 60
seconds with `waitUntil`. At expiry it waits for a fresh response. Refresh errors
cannot extend the stale window. Concurrent refreshes are coalesced within one isolate.
Cache API storage is local to a data center and does not use tiered caching.
<<< ../../__fixtures__/caching/guest-cache.ts
Worker entry (bundle with the `workerd` and `worker` resolution conditions):
<<< ../../__fixtures__/caching/worker.ts
### Equivalent Cache Rules for an origin server
Use these settings instead of the Worker wrapper when Cloudflare fronts the Express
origin directly. The origin must keep the same public representation contract.
Create the guest eligibility rule first and the bypass rule last, since matching
later rules override earlier settings. Scope both rules to your hostname.
| Rule | Matching requests | Settings |
| --- | --- | --- |
| Guest page | GET or HEAD, path exactly `/guest` | Eligible for cache; respect origin Cache-Control and bypass if absent; browser TTL respects origin; full URL key including query; serve stale while revalidating enabled |
| Credentials | Cookie string contains `session=` or Authorization header is present | Bypass cache |
| Other requests | Other paths, methods, Range, conditional headers or explicit request cache directives | Bypass cache |
The cookie substring match intentionally over-bypasses lookalike names; it does not
miss an empty `session=` value. Use the rules editor's header-presence condition for
Authorization, including empty values. Do not override origin private/no-store,
strip Set-Cookie, or force an edge TTL on credentialed responses. The origin guest
policy supplies max-age and stale-while-revalidate; using s-maxage disables stale
serving on Cloudflare. See [Cache Rules settings](https://developers.cloudflare.com/cache/how-to/cache-rules/settings/)
and [cookie bypass](https://developers.cloudflare.com/cache/how-to/cache-rules/examples/bypass-cache-on-cookie/).
## Express behind Nginx
Create the Express app with the Node renderer:
<<< ../../__fixtures__/caching/express.ts
Run this launcher with your server TypeScript build/runtime:
<<< ../../__fixtures__/caching/express-start.ts
Run Nginx with this configuration in a writable prefix directory. It listens on 8080
and proxies to Express on localhost:3000; configure production TLS and hostnames for
your deployment. Guest HTML is buffered into the proxy cache. The private path
always reaches Express. Both bypass and no-cache directives matter: the former
prevents reading a guest hit; the latter prevents storing a private response.
<<< ../../__fixtures__/caching/nginx.conf{nginx}
`$cookie_session` alone treats empty and `0` values as false, so the additional raw
cookie presence map covers those values. Nginx honors origin Cache-Control,
Set-Cookie and Vary by default. Background update uses the origin's bounded SWR
window; avoid an unconditional `proxy_cache_use_stale updating` override that could
outlive it. See the [Nginx proxy module](https://nginx.org/en/docs/http/ngx_http_proxy_module.html).
## Conditional requests and streaming
`conditionalRequest(request, { etag?, lastModified? })` returns a bodyless 304 for a
matching GET or HEAD, otherwise undefined. ETags must be quoted (optionally W/);
Last-Modified accepts a Date or date string. It supports weak ETag comparison,
comma-separated tags and `*` for an existing representation. If-None-Match takes
precedence over If-Modified-Since, including when no ETag matches. Modification
times compare at HTTP's whole-second precision. Invalid supplied validators throw;
invalid request validators are ignored. Other methods never return 304.
Call it only for an existing successful representation after selecting and authorizing
that representation. It does not implement mutation preconditions such as If-Match.
Retain the 200 response's Cache-Control, Vary and Content-Location when applicable.
The following buffered response uses an application-supplied version covering both
article and template revisions:
<<< ../../__fixtures__/caching/buffered.ts
A streamed body cannot be hashed before sending it. Setting `isStream: false` waits
for React and loader promises, but the Fetch response still exposes a body stream;
buffer/consume that body yourself before hashing it and constructing a conditional
response. This helper never reads a stream. For streaming applications, use an
application-supplied representation version known before rendering, and make the
conditional decision in a buffered/non-streaming path before committing the shell.
The version must cover every output dependency, including serialized state and
personalization. A build ID alone is insufficient for changing page data.
---
Source: https://lomray-software.github.io/vite-ssr-boost/guide/choosing
# When to use vite-ssr-boost and when not to
Use vite-ssr-boost to add SSR to a Vite app while keeping React Router [Data-mode route objects](https://reactrouter.com/start/modes). Start with the [managed CLI](/guide/getting-started), or own the transport through [runtime adapters](/guide/runtime-adapters).
The table describes configuration and migration steps, not measured effort or performance. The Next.js column covers the App Router; the Vike column covers React integration. Each cell about another project links to its official documentation.
| Topic | vite-ssr-boost | React Router Framework mode | Next.js | Vike | TanStack Start |
| --- | --- | --- | --- | --- | --- |
| Routing model | [Data-mode route objects](/guide/routing) | [Route modules and `routes.ts`](https://reactrouter.com/upgrading/router-provider) | [App Router; file-system routing](https://nextjs.org/docs/app) | [Filesystem, route strings or route functions](https://vike.dev/routing) | [TanStack Router; file-system routing](https://tanstack.com/start/latest/docs/framework/react/guide/routing) |
| Migration effort from an existing React Router SPA | [Add plugin, HTML outlet, entries and scripts](/guide/migrate-existing-spa) | [Convert routes to modules; add plugin, config and root entry](https://reactrouter.com/upgrading/router-provider) | [Migrate Vite setup to Next.js; retain React Router for SPA, move to App Router for streaming](https://nextjs.org/docs/app/guides/migrating/from-vite) | [Retain React Router during migration; migrate routing afterward](https://vike.dev/react-router) | [Convert routes, links and hooks to TanStack Router](https://tanstack.com/router/latest/docs/installation/migrate-from-react-router); [add Start routing setup](https://tanstack.com/start/latest/docs/framework/react/guide/routing) |
| Data loading | [Route loaders; see contract below](#loader-data-in-vite-ssr-boost) | [`loader` and `clientLoader`](https://reactrouter.com/start/framework/data-loading) | [Fetch in Server Components; pass props to Client Components](https://nextjs.org/docs/app/getting-started/server-and-client-components) | [`+data` and `useData()`](https://vike.dev/data-fetching) | [`createServerFn` from loaders or components](https://tanstack.com/start/latest/docs/framework/react/guide/server-functions) |
| Streaming SSR | [React streams and Suspense](/guide/runtime-adapters#streaming-and-cancellation) | [Suspense with loader promises](https://reactrouter.com/how-to/suspense) | [Server Components and streaming](https://nextjs.org/docs/app/getting-started/server-and-client-components) | [HTML streaming via `vike-react`](https://vike.dev/streaming) | [Document SSR and streaming](https://tanstack.com/start/latest/docs/framework/react/overview) |
| SPA output from the same code | [`--focus-only client`](/guide/migrate-existing-spa#going-back-to-spa) | [`ssr: false`](https://reactrouter.com/start/framework/rendering) | [`output: 'export'`; excludes server features](https://nextjs.org/docs/app/guides/single-page-applications) | [`ssr: false` via `vike-react`](https://vike.dev/ssr) | [`spa: { enabled: true }`](https://tanstack.com/start/latest/docs/framework/react/guide/spa-mode) |
| RSC | No implementation | [Opt in with `unstable_reactRouterRSC`](https://reactrouter.com/how-to/react-server-components) | [Pages and layouts use Server Components by default](https://nextjs.org/docs/app/getting-started/server-and-client-components) | [Via `vike-react-rsc`](https://vike.dev/react) | [Opt in with `rsc.enabled`](https://tanstack.com/start/latest/docs/framework/react/guide/server-components) |
| Server ownership | [CLI or transport supplied by the application](/guide/runtime-adapters) | [Node server template or Express integration](https://reactrouter.com/start/framework/deploying) | [`next start` or server supplied by the application](https://nextjs.org/docs/app/getting-started/deploying) | [`+server: true`, `+server.js` or `renderPage()`](https://vike.dev/server) | [Hosting integration or server forwarding to the Fetch entry](https://tanstack.com/start/latest/docs/framework/react/guide/hosting) |
| Hosting targets | [Node/Express CLI; Fetch runtimes via adapters; SPA files](/guide/runtime-adapters) | [Node/Docker, Vercel, Cloudflare Workers, Netlify](https://reactrouter.com/start/framework/deploying) | [Node, Docker, export files or platform adapters](https://nextjs.org/docs/app/getting-started/deploying) | [Node, Bun, Deno](https://vike.dev/self-host); [Cloudflare, Netlify, Vercel](https://vike.dev/server) | [Node/Docker, Bun, Cloudflare Workers, Netlify, Vercel](https://tanstack.com/start/latest/docs/framework/react/guide/hosting) |
The RSC implementations linked above have different release contracts: React Router [labels its support experimental](https://reactrouter.com/how-to/react-server-components), as do [Vike](https://vike.dev/react) and [TanStack Start](https://tanstack.com/start/latest/docs/framework/react/guide/server-components). Consult those pages before selecting an RSC integration.
## Loader data in vite-ssr-boost
Loader and action promises stream by default in Data mode. Use Suspense with `` or React 19 `use()` for slow fields, and opt into `hydration: 'early'` when the shell should become interactive before slow boundaries finish. See [Stream loader data](/guide/data-streaming) for the value matrix and custom-state constraint.
## Choose a project
- **Choose vite-ssr-boost when** you already have a Vite app with React Router route objects and want to keep that structure while adding SSR. The [migration guide](/guide/migrate-existing-spa) shows the entry changes; choose the Fetch path when you also want to own transport and asset delivery.
- **Choose React Router Framework mode when** you want its route module API, generated route types and rendering configuration. Its [mode guide](https://reactrouter.com/start/modes) describes that integration, and its [adoption guide](https://reactrouter.com/upgrading/router-provider) explains the route conversion it requires.
- **Choose Next.js when** you want the App Router's file-system routing, Server Components and Server Functions. Those are part of its [documented App Router model](https://nextjs.org/docs/app), while vite-ssr-boost does not implement them.
- **Choose Vike when** you want its page configuration and routing with a choice of server integration. Its [routing guide](https://vike.dev/routing) documents filesystem routes, route strings and route functions, and its [server guide](https://vike.dev/server) documents the integration points.
- **Choose TanStack Start when** you want TanStack Router with document SSR, streaming and server functions. The [Start overview](https://tanstack.com/start/latest/docs/framework/react/overview) describes that combination; use its [routing guide](https://tanstack.com/start/latest/docs/framework/react/guide/routing) to assess the route structure you would adopt.
---
Source: https://lomray-software.github.io/vite-ssr-boost/guide/cloudflare
# Cloudflare Workers
Use `@lomray/vite-ssr-boost/cloudflare` to serve a built React Router Data mode application in
Workers. It provides a Fetch handler, route asset injection, streaming HTML and access to bindings.
Keep your Express server entry for `ssr-boost dev`.
## Build a Worker entry
Install Wrangler in your application:
```bash
npm install --save-dev wrangler
```
Add an extra SSR entry to your existing Vite config. This example uses `src` as the Vite root:
```ts
import { defineConfig } from 'vite';
import SsrBoost from '@lomray/vite-ssr-boost/plugin';
export default defineConfig({
root: 'src',
publicDir: '../public',
build: { outDir: '../build' },
plugins: [
SsrBoost({
clientFile: 'client.tsx',
serverFile: 'server.tsx', // Existing Express entry for development.
entrypoint: [{ name: 'worker', type: 'ssr', serverFile: 'worker.ts' }],
}),
// Keep your existing React and other Vite plugins here.
],
});
```
Create `src/worker.ts` alongside your shared `App` and routes:
```ts
import {
createWorkerHandler,
getHtmlFromAssets,
} from '@lomray/vite-ssr-boost/cloudflare';
import manifest from '../build/client/assets-manifest.json';
import App from './App';
import routes from './routes';
export default {
fetch: createWorkerHandler({
routes,
App,
manifest,
getHtml: async (_request, env) => (await getHtmlFromAssets(env, '/index.html'))(),
}),
};
```
The exact JSON import is **`build/client/assets-manifest.json`**, relative to the Worker source.
It is the route-ID-to-assets manifest, not Vite's `.vite/manifest.json`. The CLI writes it alongside
`build/server/assets-manifest.json` after building the client and regular server. The extra Worker
entry is built afterward, so its JSON import exists on a clean build. Do not import the Worker
entry from your client or regular server entry.
Run all build entries:
```bash
npx ssr-boost build --focus-only all
```
The default `ssr-boost build` builds only the app. `--focus-only all` also builds the configured
Worker, emitting `build/worker/worker.js`. No separate Vite invocation or new server-entry flag is
needed. Rebuild all entries after changing routes or assets. The managed watch preview does not
regenerate this production manifest for a Worker.
`App` receives `{ server: appProps, children }`, matching the managed server's wrapper convention.
The handler also accepts `routerOptions`, `modulePreload`, `outlet`, and the Fetch core's lifecycle,
state, nonce, timeout, hydration and router context options. `getHtml(request, env, ctx)` may load a
custom shell. Alternatively, pass `indexHtml` containing the complete built HTML string with exactly
one `` (or your custom `outlet`); do not pass a filename as `indexHtml`.
## Wrangler configuration
Create `wrangler.jsonc` at the project root:
```jsonc
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "my-ssr-app",
"main": "build/worker/worker.js",
"compatibility_date": "2026-07-30",
"assets": {
"directory": "build/client",
"binding": "ASSETS",
"run_worker_first": true,
"html_handling": "none",
"not_found_handling": "none"
}
}
```
`run_worker_first` lets the handler render `/` and apply cache headers to static responses.
`html_handling: "none"` lets the shell helper fetch `/index.html` without a redirect. Keep
`not_found_handling: "none"` so a static miss reaches SSR and can return a real 404.
See [Workers Static Assets](https://developers.cloudflare.com/workers/static-assets/) and
[Wrangler configuration](https://developers.cloudflare.com/workers/wrangler/configuration/).
GET and HEAD requests try the binding for static files, including public files such as
`robots.txt` and extensionless files. `/` always renders; `/index.html` is also routed through SSR.
A binding 404 falls through to React Router. Other binding responses retain their status and
headers. Actions and other methods go directly to SSR. Static hits bypass SSR request hooks;
wrap the returned fetch function if your application needs authentication for static files too.
Successful files matching Vite's default `/assets/name-HASH.ext` convention (at least eight hash
characters) receive `Cache-Control: public, max-age=31536000, immutable`, including conditional
304 responses. Unhashed public files retain the binding's cache policy. Keep Vite's hashed naming
convention; configure your own cache policy if you customize asset names.
To rename the binding, use `assets: 'STATIC'` in `createWorkerHandler`, `binding: 'STATIC'` in
Wrangler, and `getHtmlFromAssets(env, '/index.html', 'STATIC')`. `assets: false` disables static
routing when another layer serves the files; you still need a shell provider.
`getHtmlFromAssets` returns `Promise<() => { header: string; footer: string }>`. It fetches once per
binding, path and outlet, coalesces concurrent loads, and supplies a fresh object for every request.
Failed loads are retried on the next call. Call it during a request, when Workers permits binding I/O.
Its fourth argument changes the outlet. Build a new Worker deployment when the HTML changes.
## Preview and deploy
```bash
# Build client, regular server, route manifest, then Worker.
npx ssr-boost build --focus-only all
# Preview the built app with local Workers bindings in workerd.
npx wrangler dev --local
# Optional: inspect the deployable bundle without publishing.
npx wrangler deploy --dry-run
# Publish the Worker and its static assets together.
npx wrangler deploy
```
Wrangler bundles the emitted Worker and dependencies. The minimal template runs without
`nodejs_compat`. Do not import `node/production`, the Express adapter, or filesystem helpers into
the Worker. The `cloudflare` entry and its library dependency graph contain no Node builtin imports.
## Bindings in hooks and loaders
Generate Worker globals with `wrangler types`, or use `@cloudflare/workers-types` in a Worker-specific
TypeScript config. Avoid mixing DOM and Workers global Fetch types in that config.
```ts
interface Env {
ASSETS: Fetcher;
MESSAGES: KVNamespace;
}
const fetch = createWorkerHandler({
App, routes, manifest,
getHtml: async (_request, env) => (await getHtmlFromAssets(env))(),
onRequest: ({ request, executionContext }) => {
const { env, ctx } = executionContext.platform;
executionContext.waitUntil(env.MESSAGES.put('last-path', new URL(request.url).pathname));
// ctx is the original Worker execution context.
return { headers: { 'X-Runtime': 'workerd' } };
},
});
export default { fetch } satisfies ExportedHandler;
```
Add your KV namespace to Wrangler with `kv_namespaces: [{ binding: 'MESSAGES', id: 'YOUR_KV_ID' }]`.
The local preview uses local KV data; seed it with
`npx wrangler kv key put --binding MESSAGES --local greeting 'Hello from KV'`.
All render hooks receive `context.executionContext`. `onRequest` and `prepare` also receive
`executionContext` directly. The default loader context is the SSR request context:
```ts
import type { LoaderFunctionArgs } from 'react-router';
import type { ISsrRequestContext } from '@lomray/vite-ssr-boost/core/render';
import type { IWorkerPlatform } from '@lomray/vite-ssr-boost/cloudflare';
export async function loader({ context }: LoaderFunctionArgs) {
const requestContext = context as unknown as ISsrRequestContext;
const { env } = requestContext.executionContext!.platform as IWorkerPlatform;
return { message: await env.MESSAGES.get('greeting') };
}
```
An explicit `routerRequestContext` replaces the default loader context. Browser navigation runs
Data mode loaders in the browser: use an HTTP endpoint for binding-backed data on client navigation.
Do not serialize bindings or the execution context with `getState`.
## Streaming and runtime limits
Streamed HTML defaults to `Content-Encoding: identity` and appends `Cache-Control: no-transform`
to preserve early chunks. In local workerd, automatic gzip buffered a small pending shell until the
loader resolved; identity delivered the shell immediately. Static assets retain normal platform
compression. You can override these defaults in `onShellReady` if you prefer platform compression;
verify chunk delivery with your application and deployment. See
[Cloudflare compression](https://developers.cloudflare.com/speed/optimization/content/compression/).
The default streams HTML and deferred loader frames. Bot user agents, including Googlebot, wait
for all content; `onRouterReady` can override `isStream`. `hydration: 'early'` enables interactive
pending shells with an async browser entry. See [Stream loader data](/guide/data-streaming).
Workers has no Early Hints transport for this handler. It never installs `onEarlyHints`; route
styles and preload links are still injected into the document.
Rendering consumes CPU time; awaiting I/O and elapsed request time are separate constraints.
Choose `abortDelay` for your rendering deadline, give loaders their own I/O deadlines, and check
your plan's [CPU, memory and duration limits](https://developers.cloudflare.com/workers/platform/limits/).
`abortDelay` begins after loader preparation and does not extend Cloudflare's limits.
Use bound `waitUntil` only for background work; its lifetime is limited too.
See [Worker execution context](https://developers.cloudflare.com/workers/runtime-apis/context/).
Workers cannot read your local build filesystem. Bundle the JSON manifest and fetch static files
through the binding. Add `nodejs_compat` only when an application dependency needs supported Node
APIs, then test that dependency in workerd. It does not turn a Worker into a Node server or give
access to local build paths. See [Node compatibility](https://developers.cloudflare.com/workers/runtime-apis/nodejs/).
## Development
Use `ssr-boost dev` for Express + Vite development, lazy route styles and HMR. Use a full build and
`wrangler dev --local` to exercise Worker bindings and streaming before deployment.
The Cloudflare Vite plugin assessment is recorded here after running the reproducible
`node scripts/probe-worker-vite.mjs` probe. It uses `cloudflare({ viteEnvironment: { name: 'ssr' } })`
with `SsrBoost()` and a source Worker with an empty development manifest. The production manifest
is generated by a build and cannot supply current development route styles.
Tested with Vite 8.2.2 and `@cloudflare/vite-plugin` 1.54.4:
| Capability | Observed result |
| --- | --- |
| Worker modules and SSR | Loaded and rendered after restarting with inline `indexHtml` and `assets: false`. |
| Assets-backed shell | The plugin rejected the helper's synthetic `/index.html` request with 403. |
| Bindings | KV reads and bound `waitUntil` worked in the inline-shell configuration. |
| Lazy route CSS | Absent from the initial SSR document. With static routing disabled, CSS requests returned 404. |
| Browser entry | Returned 404 in the inline-shell configuration; the source shell had no Vite client injection. |
| SSR reload | Editing the lazy route did not update subsequent SSR responses during the probe. |
| Browser hydration / HMR | Not verified in a browser; the missing entry and styles already prevent recommending this configuration. |
Use the managed development path. The probe's temporary inline-shell variant isolates module
loading and bindings from the asset failures.
Cloudflare's plugin supplies a Workers environment and HMR support, but the SSR BOOST integration
must also provide the development HTML, route styles and hydration behavior.
See [Cloudflare Vite plugin](https://developers.cloudflare.com/workers/vite-plugin/) and
[Vite environments](https://developers.cloudflare.com/workers/vite-plugin/reference/vite-environments/).
## Tests
From the SSR BOOST repository, after `npm run build`:
```bash
npm run test:worker:packed
npm run test:worker:browser
```
The packed test installs the tarball into a temporary minimal template, builds all entries, checks
Worker types, bundles with Wrangler, and boots workerd with Static Assets and KV. It checks routes,
cookies, deferred frames, bot buffering, binding reads, `waitUntil`, cache headers and HEAD requests.
The browser suite checks hydration, navigation, lazy CSS and interactive deferred boundaries.
---
Source: https://lomray-software.github.io/vite-ssr-boost/guide/data-streaming
# Stream loader data
Loader and action promises stream to the browser by default in React Router **Data mode**. Return critical data immediately and leave slower fields as promises. Both `` and React 19 `use()` receive native promises during hydration. Client navigations run your loaders in the browser, where their promises remain native.
## Return a promise
```tsx
import { Suspense } from 'react';
import { Await, useLoaderData } from 'react-router';
export function loader() {
return {
title: 'Deferred',
slow: new Promise<{ users: number }>((resolve) => {
setTimeout(() => resolve({ users: 3 }), 1500);
}),
};
}
export default function DeferredPage() {
const data = useLoaderData() as ReturnType;
return (
{data.title}
Loading users…
}>
Could not load users.}>
{(value) => Users: {value.users}
}
);
}
```
The same structure works in an `action`, read with `useActionData()`. React Router awaits the loader/action's own return promise before rendering: return an object containing a promise to defer a field. The transport also handles a promise at the root of an individual `loaderData` or `actionData` entry, promises nested in plain objects and arrays, and promises introduced by resolved values, including further nesting. Repeated references to a promise share one browser promise within that response.
For real requests, pass the loader's `request.signal` to `fetch`. Keep loader code usable during client navigation; put server-only work behind an API. Attach rejection handlers to work that might reject **before** `staticHandler.query()` finishes; transport handlers are installed after the query.
This follows React Router's [Suspense usage](https://reactrouter.com/how-to/suspense) and [custom Data framework architecture](https://reactrouter.com/start/data/custom).
## React 19 `use()`
Keep the consumer inside its Suspense boundary:
```tsx
import { Suspense, use } from 'react';
import { useLoaderData } from 'react-router';
function Users({ promise }: { promise: Promise<{ users: number }> }) {
const value = use(promise);
return Users: {value.users}
;
}
export default function DeferredPage() {
const data = useLoaderData() as { slow: Promise<{ users: number }> };
return (
Loading users…}>
);
}
```
Use `` on React 18. Rejected `use()` promises go to the nearest React error boundary; `` can render local failure UI and `useAsyncError()` reads its rejection.
## Supported values
Initial router data and every streamed resolution use the same [devalue](https://github.com/sveltejs/devalue) value codec, with custom promise and error reducers. The transport does not depend on React Router's internal codec exports.
| Value | Browser result |
| --- | --- |
| JSON primitives, plain objects, arrays | Same values; own string keys are preserved, including `__proto__`. |
| `undefined` | Preserved in objects and arrays. |
| `Date` | A `Date`, including invalid dates. |
| `Map`, `Set` | Native collections, with recursively decoded keys and values. |
| `bigint` | A `bigint`, without numeric precision loss. |
| `RegExp` | Same source and flags; `lastIndex` resets to zero. |
| `NaN`, positive/negative infinity, `-0`, sparse arrays | Preserved by the codec. |
| `Promise` | One native promise per request-local identity. |
| Errors and route error responses | The fields described below; arbitrary custom properties are omitted. |
| Functions, symbols, other class instances, WeakMap/WeakSet | Unsupported; diagnostics warn and these values decode as `undefined`. |
| Circular references | Outside the supported contract; diagnostics warn even where the codec can preserve a cycle. Prefer acyclic route data. |
Object identity is preserved within a value frame. Across separate settlement frames only promise identity is guaranteed. Codec features beyond this table, such as typed arrays and arbitrary custom types, are not part of the router-data contract. `getState` continues to use **JSON**, so its custom state must contain plain snapshots with no promises or rich values.
## Rejections and aborts
Ordinary errors retain `message` and `name`. React Router error responses and rejected `data(value, { status, statusText })` values retain `message`, `status`, `statusText` and `data`; reconstructed response errors also satisfy `isRouteErrorResponse`. Stacks are sent only when diagnostics are enabled. Rejections after the shell was sent cannot change the HTTP status.
`abortDelay` defaults to 15 seconds and starts after routing/preparation. It remains active until both React and pending loader/action promises finish, including promises the component never reads. On timeout, pending browser promises reject with `SSR loader/action promise aborted before it settled.` before the response closes. Development diagnostics report `SSR_BOOST_STREAM_PROMISE_ABORTED`. Disconnects cancel rendering and discard queued data when the consumer is gone. Aborting the render does not stop arbitrary application promises; use request cancellation for their underlying work.
## Hydrate the shell early
Opt in on the managed entry's lifecycle configuration:
```ts
export default entryServer(App, routes, {
init: () => ({
hydration: 'early',
getState: ({ context }) => ({ app: { locale: context.appProps.locale } }),
}),
});
```
For a Fetch handler, pass `hydration: 'early'` alongside `getHtml` and other `createHandler` options. Use an **async** client module script (`