@nuxt/docs-nightly 5.0.0-29662437.89c933ea → 5.0.0-29664396.5668d45c
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -993,3 +993,131 @@ To use this feature, you need to:
|
|
|
993
993
|
::read-more{icon="i-simple-icons-github" to="https://github.com/KazariEX/dxup" target="_blank"}
|
|
994
994
|
Learn more about **@dxup/nuxt**.
|
|
995
995
|
::
|
|
996
|
+
|
|
997
|
+
## ssrStreaming
|
|
998
|
+
|
|
999
|
+
Enables SSR streaming to dramatically improve Time to First Byte (TTFB). When enabled, the server sends the HTML shell (including `<head>`, styles, preload hints, and entry scripts) immediately, then streams the rendered body content progressively using Vue's `renderToWebStream`.
|
|
1000
|
+
|
|
1001
|
+
```ts twoslash [nuxt.config.ts]
|
|
1002
|
+
export default defineNuxtConfig({
|
|
1003
|
+
experimental: {
|
|
1004
|
+
ssrStreaming: true,
|
|
1005
|
+
},
|
|
1006
|
+
})
|
|
1007
|
+
```
|
|
1008
|
+
|
|
1009
|
+
Streaming is automatically disabled for bot and crawler user agents (such as Googlebot, Bingbot, etc.) to ensure search engines receive fully-rendered HTML for SEO safety. The default pattern matches indexing crawlers only; Lighthouse and other audit tools deliberately fall outside it so synthetic measurements reflect the same streamed response real users get. You can customize the bot detection regex:
|
|
1010
|
+
|
|
1011
|
+
```ts twoslash [nuxt.config.ts]
|
|
1012
|
+
export default defineNuxtConfig({
|
|
1013
|
+
experimental: {
|
|
1014
|
+
ssrStreaming: {
|
|
1015
|
+
botRegex: /googlebot|bingbot|my-internal-crawler/i,
|
|
1016
|
+
},
|
|
1017
|
+
},
|
|
1018
|
+
})
|
|
1019
|
+
```
|
|
1020
|
+
|
|
1021
|
+
You can also control streaming per-route using `routeRules`:
|
|
1022
|
+
|
|
1023
|
+
```ts twoslash [nuxt.config.ts]
|
|
1024
|
+
export default defineNuxtConfig({
|
|
1025
|
+
experimental: {
|
|
1026
|
+
ssrStreaming: true,
|
|
1027
|
+
},
|
|
1028
|
+
routeRules: {
|
|
1029
|
+
'/no-stream/**': { streaming: false },
|
|
1030
|
+
},
|
|
1031
|
+
})
|
|
1032
|
+
```
|
|
1033
|
+
|
|
1034
|
+
::warning
|
|
1035
|
+
**Automatic fallback to non-streamed rendering.** Streaming commits the response status and headers as soon as the shell is flushed, which is incompatible with features that need to mutate the response after render. Requests matching any of the following are not streamed; they use the buffered renderer, or short-circuit to a redirect or error response:
|
|
1036
|
+
|
|
1037
|
+
- `routeRules` setting `noScripts`, `cache`, `isr`, `swr`, `redirect`, or `streaming: false` for the route
|
|
1038
|
+
- `ssr: false` routes (already SPA-rendered)
|
|
1039
|
+
- Bot/crawler user agents (controlled via `botRegex`)
|
|
1040
|
+
- Prerendered routes (`nuxi generate`)
|
|
1041
|
+
- Server-side `navigateTo()` redirects from plugins, middleware, or page setup
|
|
1042
|
+
- Fatal errors thrown during initial render (before the shell flushes)
|
|
1043
|
+
::
|
|
1044
|
+
|
|
1045
|
+
::warning
|
|
1046
|
+
**Response status and headers must be set before the shell is flushed.** Streaming commits the HTTP status and headers with the first byte, so anything that mutates the response after that point cannot reach the client. This is inherent to streaming, not a Nuxt-specific bug.
|
|
1047
|
+
|
|
1048
|
+
The boundary is the shell flush:
|
|
1049
|
+
|
|
1050
|
+
- **Reaches the client**: mutations from Nuxt and Nitro plugins, which run to completion before rendering begins.
|
|
1051
|
+
- **Dropped**: `setResponseStatus()`, `useResponseHeader()`, `useCookie()` writes and h3 `setHeader()`/`appendResponseHeader()` calls made during component rendering (including after an `await` in route middleware or `<script setup>`), since that work happens after the shell is already on the wire.
|
|
1052
|
+
|
|
1053
|
+
To keep a response mutation, move it into a plugin, or opt the route out of streaming:
|
|
1054
|
+
|
|
1055
|
+
- `routeRules: { '/path': { streaming: false } }`: static, per route.
|
|
1056
|
+
- the `render:route` hook with `ctx.prefersStream = false`: runtime, per request (e.g. for routes that conditionally set a 404).
|
|
1057
|
+
|
|
1058
|
+
In development the streaming handler logs a warning naming the dropped mutations and the route, so these never fail silently.
|
|
1059
|
+
::
|
|
1060
|
+
|
|
1061
|
+
::note
|
|
1062
|
+
If an error occurs during streaming after the HTTP status is already committed, `payload.error` is set and the closing tags are still emitted as a well-formed document so the client picks up the error during hydration and renders the error page. Errors thrown before the shell flushes fall through to the buffered error renderer with the correct status code.
|
|
1063
|
+
::
|
|
1064
|
+
|
|
1065
|
+
::note
|
|
1066
|
+
**Route styles are streamed, JS hints are entry-only.** The shell is flushed before the route renders, so its `<head>` carries entry-chunk styles and hints only. Once render registers the page and layout modules, their CSS is streamed straight after the shell (inlined as `<style>` when `inlineStyles` is enabled, otherwise as stylesheet links), so page, layout, and top-level async-component styles arrive before the body paints (nested async components are a FOUC caveat, covered below). Route-specific JS chunks are not preloaded from the shell; the browser discovers them after parsing the entry script. Streaming improves TTFB on every route; LCP gains are largest on routes whose JS overlaps the entry chunk.
|
|
1067
|
+
::
|
|
1068
|
+
|
|
1069
|
+
::note
|
|
1070
|
+
**Component islands are compatible with streaming.** Island slot content and selective-client (`nuxt-client`) components are normally stitched into the HTML in a post-render pass, which is impossible once the body has streamed past the island anchors. Instead, the renderer emits each island teleport as an inert `<template>` at the end of the document and relocates it into place with an inline script that runs before hydration. This is transparent to app code. The exception is apps built with `features.noScripts` and island components, which fall back to the buffered renderer since the relocation script cannot run.
|
|
1071
|
+
::
|
|
1072
|
+
|
|
1073
|
+
### Module hooks
|
|
1074
|
+
|
|
1075
|
+
Modules participate in the streaming response via the existing `render:html` hook (now with a `streaming: true` flag on the second argument) plus a per-request decision hook and two streaming-only hooks:
|
|
1076
|
+
|
|
1077
|
+
- **`render:route`** fires once per request before rendering begins, for every render (streaming enabled or not). Read `ctx.canStream` to see whether streaming is possible for the route, and set `ctx.prefersStream = false` to force buffered rendering for this request, e.g. based on a cookie, auth state, or A/B bucket. The renderer streams only when `canStream && prefersStream`. This is the runtime escape hatch for the static `routeRules` / `botRegex` config.
|
|
1078
|
+
- **`render:html`** fires once, before the shell flushes, with `streaming: true` on its second argument. Mutations to `htmlAttrs`, `head`, `bodyAttrs`, and `bodyPrepend` reach the wire. Mutations to `body`/`bodyAppend` are dropped, since the body is about to stream (a dev-mode warning is emitted). Modules that only mutate head fields (CSP injection, OG tags, analytics meta) work in streaming with no code changes.
|
|
1079
|
+
- **`render:html:chunk`** fires for each chunk produced by the renderer before it is enqueued. Mutate `ctx.chunk: Uint8Array` to transform bytes (e.g. nonce injection); read `ctx.index` to identify the first chunk vs. subsequent ones.
|
|
1080
|
+
- **`render:html:close`** fires after the body stream completes, before closing tags. Mutate `ctx.bodyAppend: string[]` to inject final markup (end-of-body analytics tags, server-rendered debug widgets, etc.).
|
|
1081
|
+
|
|
1082
|
+
```ts
|
|
1083
|
+
// modules/streaming-csp/src/runtime/server-plugin.ts
|
|
1084
|
+
import { defineNitroPlugin } from '#imports'
|
|
1085
|
+
|
|
1086
|
+
export default defineNitroPlugin((nitro) => {
|
|
1087
|
+
nitro.hooks.hook('render:html', (ctx, { event }) => {
|
|
1088
|
+
const nonce = event.context.cspNonce
|
|
1089
|
+
if (!nonce) { return }
|
|
1090
|
+
// Works for both streaming (pre-shell) and buffered (post-render) paths.
|
|
1091
|
+
for (let i = 0; i < ctx.head.length; i++) {
|
|
1092
|
+
ctx.head[i] = ctx.head[i].replace(/<script(?![^>]*\snonce=)/g, `<script nonce="${nonce}"`)
|
|
1093
|
+
}
|
|
1094
|
+
})
|
|
1095
|
+
})
|
|
1096
|
+
```
|
|
1097
|
+
|
|
1098
|
+
::note
|
|
1099
|
+
**CSP nonce.** The streaming renderer emits several inline scripts and styles that bypass unhead: the bootstrap queue, the IIFE, suspense head pushes, island-teleport relocation, and route `<style>` blocks. If a `nonce` is present on the rendered head scripts, the renderer reuses it on all of them automatically, so a strict `script-src`/`style-src 'nonce-…'` policy does not block streaming. A module only needs to put the nonce on the head scripts (as above); the `render:html:chunk` hook remains available for stamping scripts that components render into the body.
|
|
1100
|
+
::
|
|
1101
|
+
|
|
1102
|
+
::warning
|
|
1103
|
+
**Dev-mode FOUC for SFC styles:** in development, Vite serves SFC `<style>` blocks as JavaScript modules that inject styles client-side after the module evaluates, with no corresponding `<link>` in the shell. With streaming, the browser starts painting the streamed DOM before those style-injection modules run, so SFC-defined styles flash unstyled briefly.
|
|
1104
|
+
|
|
1105
|
+
Workaround: put paint-critical styles in a global CSS file registered via `css: ['~/assets/main.css']`. Global CSS files are emitted as `<link rel="stylesheet">` in the shell `<head>` and apply before body content streams. SFC `<style>` blocks remain fine for component-scoped styling that doesn't gate the initial paint.
|
|
1106
|
+
|
|
1107
|
+
Production builds extract all styles to real CSS files (or inline via `features.inlineStyles`), so this only affects `nuxt dev`. Validate streaming visuals against `nuxt build && nuxt preview`.
|
|
1108
|
+
::
|
|
1109
|
+
|
|
1110
|
+
::warning
|
|
1111
|
+
**Production FOUC for nested async components:** the renderer inlines route CSS in a chunk sent straight after the shell. It can only inline the styles for components whose modules are already registered at that point: the page, the layout, and any async component placed directly inside a `<Suspense>` boundary (Vue instantiates those eagerly when render begins).
|
|
1112
|
+
|
|
1113
|
+
An async component that is rendered *inside another async component* is instantiated only once its parent resolves, after the first chunk has streamed. Its SFC `<style>` misses the post-shell styles chunk and is emitted in the closing HTML instead, behind the component's own DOM. The browser paints that component unstyled until the final chunk arrives.
|
|
1114
|
+
|
|
1115
|
+
Avoid it by keeping paint-critical styling out of deeply nested async components:
|
|
1116
|
+
|
|
1117
|
+
- Put styles that gate the initial paint in a global CSS file (`css: ['~/assets/main.css']`); these reach the shell `<head>`.
|
|
1118
|
+
- Style with utility classes (Tailwind, UnoCSS): utility CSS lives in the entry stylesheet, not per-component `<style>` blocks.
|
|
1119
|
+
- Keep async components that own paint-critical `<style>` directly under a `<Suspense>` boundary rather than nested behind another async parent.
|
|
1120
|
+
- Or opt the route out of streaming with `routeRules: { '/path': { streaming: false } }`.
|
|
1121
|
+
|
|
1122
|
+
Non-paint-critical scoped styles on nested async components are fine: the brief flash only matters for above-the-fold content.
|
|
1123
|
+
::
|