@timber-js/app 0.2.0-alpha.32 → 0.2.0-alpha.33

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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/index.ts"],"names":[],"mappings":"AA0FA;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAE/F;AAoZD,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAIzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;8BAtQ3C,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC;AAwQhD,wBAAiE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/index.ts"],"names":[],"mappings":"AA2FA;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAE/F;AAkaD,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAIzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;8BApR3C,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC;AAsRhD,wBAAiE"}
@@ -1 +1 @@
1
- {"version":3,"file":"ssr-renderer.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/ssr-renderer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AAGxE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAYrE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAErD,UAAU,gBAAgB;IACxB,GAAG,EAAE,OAAO,CAAC;IACb,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IACtC,OAAO,EAAE,aAAa,CAAC;IACvB,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,gBAAgB,EAAE,oBAAoB,EAAE,CAAC;IACzC,KAAK,EAAE,UAAU,CAAC;IAClB,eAAe,EAAE,OAAO,CAAC;IACzB,eAAe,EAAE,qBAAqB,CAAC;IACvC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAsKjF"}
1
+ {"version":3,"file":"ssr-renderer.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/ssr-renderer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AAGxE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAYrE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGrD,UAAU,gBAAgB;IACxB,GAAG,EAAE,OAAO,CAAC;IACb,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IACtC,OAAO,EAAE,aAAa,CAAC;IACvB,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,gBAAgB,EAAE,oBAAoB,EAAE,CAAC;IACzC,KAAK,EAAE,UAAU,CAAC;IAClB,eAAe,EAAE,OAAO,CAAC;IACzB,eAAe,EAAE,qBAAqB,CAAC;IACvC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAqLjF"}
@@ -53,6 +53,28 @@ export interface NavContext {
53
53
  * to a page-level deny. See LOCAL-298.
54
54
  */
55
55
  _denyHandledByBoundary?: boolean;
56
+ /**
57
+ * Mutable: SSR timing data populated by handleSsr().
58
+ * Read by the RSC entry to record sub-phase Server-Timing entries
59
+ * when `serverTiming: 'detailed'` is configured.
60
+ *
61
+ * This bridges the RSC→SSR environment boundary: the SSR entry populates
62
+ * these fields, the RSC entry reads them after callSsr() returns.
63
+ */
64
+ _ssrTimings?: {
65
+ /** Time to decode RSC stream (createFromReadableStream/createFromNodeStream) */
66
+ decodeMs: number;
67
+ /** Time for Fizz to render the shell (onShellReady) */
68
+ shellMs: number;
69
+ /** Time for pipe() to flush shell bytes to the output stream */
70
+ pipeMs: number;
71
+ /** Time to set up Node.js Transform pipeline / Web Stream transforms */
72
+ pipelineMs: number;
73
+ /** Total SSR time (decode → response ready) */
74
+ totalMs: number;
75
+ /** Whether the Node.js native stream path was used */
76
+ nodeStreams: boolean;
77
+ };
56
78
  }
57
79
  /**
58
80
  * Handle SSR: decode an RSC stream and render it to hydration-ready HTML.
@@ -1 +1 @@
1
- {"version":3,"file":"ssr-entry.d.ts","sourceRoot":"","sources":["../../src/server/ssr-entry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AA0EH;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC1C,iCAAiC;IACjC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,eAAe,EAAE,OAAO,CAAC;IACzB,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,sBAAsB,EAAE,MAAM,CAAC;IAC/B,qEAAqE;IACrE,SAAS,CAAC,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IACvC;;;0DAGsD;IACtD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;iFAE6E;IAC7E,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;4DACwD;IACxD,OAAO,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,SAAS,CAC7B,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,EACrC,UAAU,EAAE,UAAU,GACrB,OAAO,CAAC,QAAQ,CAAC,CAyJnB;AAED,eAAe,SAAS,CAAC"}
1
+ {"version":3,"file":"ssr-entry.d.ts","sourceRoot":"","sources":["../../src/server/ssr-entry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AA0EH;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC1C,iCAAiC;IACjC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,eAAe,EAAE,OAAO,CAAC;IACzB,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,sBAAsB,EAAE,MAAM,CAAC;IAC/B,qEAAqE;IACrE,SAAS,CAAC,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IACvC;;;0DAGsD;IACtD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;iFAE6E;IAC7E,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;4DACwD;IACxD,OAAO,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;IAEjC;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE;QACZ,gFAAgF;QAChF,QAAQ,EAAE,MAAM,CAAC;QACjB,uDAAuD;QACvD,OAAO,EAAE,MAAM,CAAC;QAChB,gEAAgE;QAChE,MAAM,EAAE,MAAM,CAAC;QACf,wEAAwE;QACxE,UAAU,EAAE,MAAM,CAAC;QACnB,+CAA+C;QAC/C,OAAO,EAAE,MAAM,CAAC;QAChB,sDAAsD;QACtD,WAAW,EAAE,OAAO,CAAC;KACtB,CAAC;CACH;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,SAAS,CAC7B,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,EACrC,UAAU,EAAE,UAAU,GACrB,OAAO,CAAC,QAAQ,CAAC,CAuJnB;AAED,eAAe,SAAS,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"ssr-render.d.ts","sourceRoot":"","sources":["../../src/server/ssr-render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAqEvC;;;;;;;;;;;;;GAaG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,SAAS,EAClB,OAAO,CAAC,EAAE;IAAE,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,GAC7F,OAAO,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC,CAErC;AAED,wEAAwE;AACxE,eAAO,MAAM,cAAc,SAAkB,CAAC;AAU9C;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,SAAS,EAClB,OAAO,CAAC,EAAE;IAAE,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,GAC7F,OAAO,CAAC,OAAO,aAAa,EAAE,QAAQ,CAAC,CAyDzC;AAED,6EAA6E;AAC7E,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,OAAO,aAAa,EAAE,QAAQ,GACvC,cAAc,CAAC,UAAU,CAAC,CAE5B;AA0CD;;;;;;;;;;;;GAYG;AACH,2CAA2C;AAC3C,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,EAClC,MAAM,CAAC,EAAE,WAAW,GACnB,cAAc,CAAC,UAAU,CAAC,CA2B5B;AAWD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,cAAc,CAAC,UAAU,CAAC,EACtC,UAAU,EAAE,MAAM,EAClB,eAAe,EAAE,OAAO,GACvB,QAAQ,CASV"}
1
+ {"version":3,"file":"ssr-render.d.ts","sourceRoot":"","sources":["../../src/server/ssr-render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAqEvC;;;;;;;;;;;;;GAaG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,SAAS,EAClB,OAAO,CAAC,EAAE;IAAE,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,GAC7F,OAAO,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC,CAErC;AAED,wEAAwE;AACxE,eAAO,MAAM,cAAc,SAAkB,CAAC;AAU9C;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,SAAS,EAClB,OAAO,CAAC,EAAE;IAAE,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,GAC7F,OAAO,CAAC,OAAO,aAAa,EAAE,QAAQ,CAAC,CAkDzC;AAED,6EAA6E;AAC7E,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,OAAO,aAAa,EAAE,QAAQ,GACvC,cAAc,CAAC,UAAU,CAAC,CAE5B;AA0CD;;;;;;;;;;;;GAYG;AACH,2CAA2C;AAC3C,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,EAClC,MAAM,CAAC,EAAE,WAAW,GACnB,cAAc,CAAC,UAAU,CAAC,CA2B5B;AAWD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,cAAc,CAAC,UAAU,CAAC,EACtC,UAAU,EAAE,MAAM,EAClB,eAAe,EAAE,OAAO,GACvB,QAAQ,CASV"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timber-js/app",
3
- "version": "0.2.0-alpha.32",
3
+ "version": "0.2.0-alpha.33",
4
4
  "description": "Vite-native React framework for Cloudflare Workers — correct HTTP semantics, real status codes, pages that work without JavaScript",
5
5
  "keywords": [
6
6
  "cloudflare-workers",
@@ -68,6 +68,7 @@ import { renderRscStream } from './rsc-stream.js';
68
68
  import { renderSsrResponse } from './ssr-renderer.js';
69
69
  import { callSsr } from './ssr-bridge.js';
70
70
  import { isDebug, isDevMode, setDebugFromConfig } from '#/server/debug.js';
71
+ import { recordTiming } from '#/server/server-timing.js';
71
72
 
72
73
  /**
73
74
  * Resolve the Server-Timing mode from timber.config.ts.
@@ -325,6 +326,7 @@ async function renderRoute(
325
326
  // Build the React element tree — loads modules, runs access checks,
326
327
  // resolves metadata. DenySignal/RedirectSignal propagate for HTTP handling.
327
328
  let routeResult;
329
+ const _buildStart = performance.now();
328
330
  try {
329
331
  routeResult = await buildRouteElement(_req, match, interception, clientStateTree);
330
332
  } catch (error) {
@@ -364,6 +366,13 @@ async function renderRoute(
364
366
  throw error;
365
367
  }
366
368
 
369
+ const _buildEnd = performance.now();
370
+ recordTiming({
371
+ name: 'build',
372
+ dur: Math.round(_buildEnd - _buildStart),
373
+ desc: 'build element tree',
374
+ });
375
+
367
376
  const { element, headElements, layoutComponents, deferSuspenseFor, skippedSegments } =
368
377
  routeResult;
369
378
 
@@ -416,7 +425,13 @@ async function renderRoute(
416
425
  }
417
426
 
418
427
  // Render to RSC Flight stream with signal tracking.
428
+ const _rscStart = performance.now();
419
429
  const { rscStream, signals } = renderRscStream(element, _req);
430
+ recordTiming({
431
+ name: 'rsc-init',
432
+ dur: Math.round(performance.now() - _rscStart),
433
+ desc: 'RSC stream init',
434
+ });
420
435
 
421
436
  // Synchronous redirect — redirect() in access.ts or a non-async component
422
437
  // throws during renderToReadableStream creation. Return HTTP redirect.
@@ -31,6 +31,7 @@ import {
31
31
  import { renderErrorPage } from './error-renderer.js';
32
32
  import { callSsr } from './ssr-bridge.js';
33
33
  import type { RenderSignals } from './rsc-stream.js';
34
+ import { recordTiming } from '#/server/server-timing.js';
34
35
 
35
36
  interface SsrRenderOptions {
36
37
  req: Request;
@@ -156,6 +157,21 @@ export async function renderSsrResponse(opts: SsrRenderOptions): Promise<Respons
156
157
  try {
157
158
  const ssrResponse = await callSsr(ssrStream, navContext);
158
159
 
160
+ // Record SSR sub-phase timings for Server-Timing header (detailed mode).
161
+ // These are populated by handleSsr() in the SSR environment and passed
162
+ // back via navContext._ssrTimings across the RSC→SSR boundary.
163
+ if (navContext._ssrTimings) {
164
+ const t = navContext._ssrTimings;
165
+ recordTiming({ name: 'ssr-decode', dur: t.decodeMs, desc: 'RSC Flight decode' });
166
+ recordTiming({ name: 'ssr-shell', dur: t.shellMs, desc: 'Fizz onShellReady' });
167
+ recordTiming({ name: 'ssr-pipeline', dur: t.pipelineMs, desc: 'stream transforms' });
168
+ recordTiming({
169
+ name: 'ssr-total',
170
+ dur: t.totalMs,
171
+ desc: t.nodeStreams ? 'SSR (Node streams)' : 'SSR (Web Streams)',
172
+ });
173
+ }
174
+
159
175
  // Signal promotion: check if any signals were captured during rendering
160
176
  // inside Suspense boundaries. If no signals are present yet, yield one
161
177
  // microtask so async component rejections propagate to the RSC onError
@@ -126,6 +126,29 @@ export interface NavContext {
126
126
  * to a page-level deny. See LOCAL-298.
127
127
  */
128
128
  _denyHandledByBoundary?: boolean;
129
+
130
+ /**
131
+ * Mutable: SSR timing data populated by handleSsr().
132
+ * Read by the RSC entry to record sub-phase Server-Timing entries
133
+ * when `serverTiming: 'detailed'` is configured.
134
+ *
135
+ * This bridges the RSC→SSR environment boundary: the SSR entry populates
136
+ * these fields, the RSC entry reads them after callSsr() returns.
137
+ */
138
+ _ssrTimings?: {
139
+ /** Time to decode RSC stream (createFromReadableStream/createFromNodeStream) */
140
+ decodeMs: number;
141
+ /** Time for Fizz to render the shell (onShellReady) */
142
+ shellMs: number;
143
+ /** Time for pipe() to flush shell bytes to the output stream */
144
+ pipeMs: number;
145
+ /** Time to set up Node.js Transform pipeline / Web Stream transforms */
146
+ pipelineMs: number;
147
+ /** Total SSR time (decode → response ready) */
148
+ totalMs: number;
149
+ /** Whether the Node.js native stream path was used */
150
+ nodeStreams: boolean;
151
+ };
129
152
  }
130
153
 
131
154
  /**
@@ -182,11 +205,8 @@ export async function handleSsr(
182
205
  // createFromReadableStream resolves client component references
183
206
  // (from "use client" modules) using the SSR environment's module
184
207
  // map, importing the actual components for server-side rendering.
185
- const _s0 = performance.now();
186
- // eslint-disable-next-line no-console
187
- console.log(
188
- `[diag] nodeImports=${!!_nodeStreamImports} nodeStreamDecode=${hasNodeStreamDecode} rscStream=${rscStream?.constructor?.name}`
189
- );
208
+ const _ssrStart = performance.now();
209
+
190
210
  // Decode the RSC stream into a React element tree.
191
211
  // On Node.js: convert Web ReadableStream → Node Readable → createFromNodeStream
192
212
  // (eliminates Promise-per-chunk overhead from Web Streams reader)
@@ -200,21 +220,13 @@ export async function handleSsr(
200
220
  } else {
201
221
  element = createFromReadableStream(rscStream) as React.ReactNode;
202
222
  }
203
- const _s1 = performance.now();
223
+ const _decodeEnd = performance.now();
204
224
 
205
225
  // Wrap with a server-safe nuqs adapter so that 'use client' components
206
226
  // that call nuqs hooks (useQueryStates, useQueryState) can SSR correctly.
207
- // The client-side TimberNuqsAdapter (injected by browser-entry.ts) takes
208
- // over after hydration. This provider supplies the request's search params
209
- // as a static snapshot so nuqs renders the right initial values on the server.
210
227
  const wrappedElement = withNuqsSsrAdapter(navContext.searchParams, element);
211
- const _s2 = performance.now();
212
228
 
213
229
  // Render to HTML stream (waits for onShellReady).
214
- // Pass bootstrapScriptContent so React injects a non-deferred <script>
215
- // in the shell HTML. This executes immediately during parsing — even
216
- // while Suspense boundaries are still streaming — triggering module
217
- // loading via dynamic import() so hydration can start early.
218
230
  //
219
231
  // Two paths based on platform:
220
232
  // - Node.js: renderToPipeableStream → Node Transform pipeline → Readable.toWeb() → Response
@@ -231,7 +243,6 @@ export async function handleSsr(
231
243
  PassThrough,
232
244
  } = _nodeStreamImports;
233
245
 
234
- const _s3 = performance.now();
235
246
  let nodeHtmlStream: import('node:stream').Readable;
236
247
  try {
237
248
  nodeHtmlStream = await renderSsrNodeStream(wrappedElement, {
@@ -249,29 +260,27 @@ export async function handleSsr(
249
260
  renderError
250
261
  );
251
262
  }
263
+ const _renderEnd = performance.now();
252
264
 
253
- // Build Node.js Transform pipeline: errorHandler → headInjector → flightInjector → gzip
254
265
  const errorHandler = createNodeErrorHandler(navContext.signal);
255
266
  const headInjector = createNodeHeadInjector(navContext.headHtml);
256
267
  const flightInjector = createNodeFlightInjector(navContext.rscStream);
257
268
 
258
- // Pipe through the chain. pipeline() handles backpressure and error propagation.
259
- // The last stream in the chain is the output — convert to Web ReadableStream
260
- // only at the Response boundary.
261
- // Note: gzip compression is still handled by compressResponse() in the Nitro
262
- // entry via Web Streams CompressionStream. Moving it into this Node.js pipeline
263
- // requires the request headers (Accept-Encoding) which NavContext doesn't carry.
264
- // TODO: pass request headers through NavContext to enable inline Node.js gzip.
265
269
  const output = new PassThrough();
266
270
  pipeline(nodeHtmlStream, errorHandler, headInjector, flightInjector, output).catch(() => {
267
271
  // Pipeline errors are handled by errorHandler transform
268
272
  });
273
+ const _pipelineEnd = performance.now();
269
274
 
270
- const _s4 = performance.now();
271
- // eslint-disable-next-line no-console
272
- console.log(
273
- `[ssr-perf] decode=${(_s1 - _s0).toFixed(1)}ms nuqs=${(_s2 - _s1).toFixed(1)}ms imports=${(_s3 - _s2).toFixed(1)}ms renderToPipeable=${(_s4 - _s3).toFixed(1)}ms pipeline=${(performance.now() - _s4).toFixed(1)}ms total=${(performance.now() - _s0).toFixed(1)}ms`
274
- );
275
+ // Record SSR sub-timings for Server-Timing header (detailed mode).
276
+ navContext._ssrTimings = {
277
+ decodeMs: Math.round(_decodeEnd - _ssrStart),
278
+ shellMs: Math.round(_renderEnd - _decodeEnd),
279
+ pipeMs: 0, // pipe() timing is inside renderSsrNodeStream
280
+ pipelineMs: Math.round(_pipelineEnd - _renderEnd),
281
+ totalMs: Math.round(_pipelineEnd - _ssrStart),
282
+ nodeStreams: true,
283
+ };
275
284
 
276
285
  const webStream = nodeReadableToWeb(output);
277
286
  return buildSsrResponse(webStream, navContext.statusCode, navContext.responseHeaders);
@@ -296,10 +305,22 @@ export async function handleSsr(
296
305
  );
297
306
  }
298
307
 
308
+ const _renderEnd = performance.now();
309
+
299
310
  // Inject metadata into <head>, then interleave RSC payload chunks
300
311
  // into the body as they arrive from the tee'd RSC stream.
301
312
  let outputStream = injectHead(htmlStream, navContext.headHtml);
302
313
  outputStream = injectRscPayload(outputStream, navContext.rscStream);
314
+ const _pipelineEnd = performance.now();
315
+
316
+ navContext._ssrTimings = {
317
+ decodeMs: Math.round(_decodeEnd - _ssrStart),
318
+ shellMs: Math.round(_renderEnd - _decodeEnd),
319
+ pipeMs: 0,
320
+ pipelineMs: Math.round(_pipelineEnd - _renderEnd),
321
+ totalMs: Math.round(_pipelineEnd - _ssrStart),
322
+ nodeStreams: false,
323
+ };
303
324
 
304
325
  // Build and return the Response.
305
326
  return buildSsrResponse(outputStream, navContext.statusCode, navContext.responseHeaders);
@@ -136,7 +136,6 @@ export async function renderSsrNodeStream(
136
136
  const deferMs = options?.deferSuspenseFor;
137
137
 
138
138
  return new Promise<import('node:stream').Readable>((resolve, reject) => {
139
- const _startTime = performance.now();
140
139
  const passthrough = new _PassThrough!();
141
140
 
142
141
  let allReadyResolve: (() => void) | null = null;
@@ -149,20 +148,14 @@ export async function renderSsrNodeStream(
149
148
  bootstrapScriptContent: options?.bootstrapScriptContent || undefined,
150
149
 
151
150
  onShellReady() {
152
- const _shellReady = performance.now();
153
151
  if (deferMs && deferMs > 0) {
154
152
  Promise.race([allReady, new Promise<void>((r) => setTimeout(r, deferMs))]).then(() => {
155
153
  pipe(passthrough);
156
154
  resolve(passthrough);
157
155
  });
158
156
  } else {
159
- const _beforePipe = performance.now();
160
157
  pipe(passthrough);
161
- const _afterPipe = performance.now();
162
158
  resolve(passthrough);
163
- const _afterResolve = performance.now();
164
- // eslint-disable-next-line no-console
165
- console.log(`[ssr-perf] onShellReady=${(_shellReady - _startTime).toFixed(1)}ms pipe=${(_afterPipe - _beforePipe).toFixed(1)}ms resolve=${(_afterResolve - _afterPipe).toFixed(1)}ms`);
166
159
  }
167
160
  },
168
161