@agent-native/core 0.146.5 → 0.146.7
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.
- package/corpus/templates/analytics/server/jobs/analytics-bigquery-backfill.ts +13 -9
- package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +69 -33
- package/corpus/templates/calendar/app/root.tsx +0 -1
- package/corpus/templates/clips/app/root.tsx +0 -1
- package/corpus/templates/content/app/root.tsx +0 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/deploy/build.d.ts +21 -0
- package/dist/deploy/build.js +185 -10
- package/dist/jobs/background-automation-runner.d.ts +1 -1
- package/dist/jobs/background-automation-runner.js +1 -1
- package/dist/jobs/scheduler-health.d.ts +4 -0
- package/dist/jobs/scheduler-health.js +33 -0
- package/dist/localization/server.d.ts +0 -1
- package/dist/localization/server.js +3 -1
- package/dist/mcp-client/index.d.ts +1 -1
- package/dist/mcp-client/index.js +1 -1
- package/dist/mcp-client/routes.d.ts +11 -0
- package/dist/mcp-client/routes.js +26 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/provider-api/actions/custom-provider-registration.d.ts +6 -6
- package/dist/provider-api/actions/provider-api.d.ts +5 -5
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-chat/mcp-glue.js +12 -3
- package/dist/server/agent-chat-plugin.js +73 -48
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/auth.js +7 -0
- package/dist/server/http-response-telemetry.js +112 -5
- package/dist/server/release-migrations.js +2 -0
- package/dist/server/ssr-handler.js +8 -0
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/templates/workspace-core/.agents/skills/performance/SKILL.md +54 -3
- package/dist/vite/client.js +5 -1
- package/package.json +1 -1
- package/src/templates/workspace-core/.agents/skills/performance/SKILL.md +54 -3
|
@@ -13,10 +13,21 @@ const TRACKING_INGEST_PATHS = new Set([
|
|
|
13
13
|
"/api/events/track",
|
|
14
14
|
"/_agent-native/track",
|
|
15
15
|
]);
|
|
16
|
+
const SLOW_REQUEST_MS = 1_000;
|
|
17
|
+
const SLOW_REQUEST_LOG_EVENT = "agent-native.slow_request";
|
|
16
18
|
const PROCESS_STATE_KEY = Symbol.for("@agent-native/core/http-response-telemetry.process-state");
|
|
17
19
|
const globalRef = globalThis;
|
|
20
|
+
// Process start → this module being evaluated. On a serverless cold start that
|
|
21
|
+
// span is the platform's container boot plus server-bundle evaluation, which
|
|
22
|
+
// happens entirely before any request handler runs and is therefore invisible
|
|
23
|
+
// to every in-handler measurement. Recorded here because module scope is the
|
|
24
|
+
// earliest point our own code can observe. Stored on globalThis so the
|
|
25
|
+
// earliest-evaluated copy wins if the bundle loads this module twice.
|
|
18
26
|
const processState = globalRef[PROCESS_STATE_KEY] ??
|
|
19
|
-
(globalRef[PROCESS_STATE_KEY] = {
|
|
27
|
+
(globalRef[PROCESS_STATE_KEY] = {
|
|
28
|
+
requestSequence: 0,
|
|
29
|
+
moduleEvalUptimeMs: Math.max(0, Math.round(process.uptime() * 1_000)),
|
|
30
|
+
});
|
|
20
31
|
const REQUEST_TELEMETRY_KEY = Symbol.for("@agent-native/core/http-response-telemetry.request");
|
|
21
32
|
const installedApps = new WeakSet();
|
|
22
33
|
function envValue(key) {
|
|
@@ -118,7 +129,7 @@ function shouldTrack(pathname, statusCode, state) {
|
|
|
118
129
|
return true;
|
|
119
130
|
if (state.startupDb)
|
|
120
131
|
return true;
|
|
121
|
-
if (Date.now() - state.startedAt >=
|
|
132
|
+
if (Date.now() - state.startedAt >= SLOW_REQUEST_MS)
|
|
122
133
|
return true;
|
|
123
134
|
if (state.db.errorCount > 0 || state.db.timeoutCount > 0) {
|
|
124
135
|
return true;
|
|
@@ -151,6 +162,10 @@ function runtimeProvider() {
|
|
|
151
162
|
return "cloudflare-pages";
|
|
152
163
|
return "node";
|
|
153
164
|
}
|
|
165
|
+
/** Module evaluation → this request starting, i.e. idle boot the app paid for. */
|
|
166
|
+
function moduleToRequestMs(state) {
|
|
167
|
+
return Math.max(0, state.processAgeAtStartMs - processState.moduleEvalUptimeMs);
|
|
168
|
+
}
|
|
154
169
|
function emitTelemetry(event, state, response) {
|
|
155
170
|
const statusCode = responseStatusCode(event, response);
|
|
156
171
|
const pathname = requestPath(event);
|
|
@@ -175,6 +190,8 @@ function emitTelemetry(event, state, response) {
|
|
|
175
190
|
cold_start: state.requestSequence === 1,
|
|
176
191
|
request_sequence: state.requestSequence,
|
|
177
192
|
process_age_ms: state.processAgeAtStartMs,
|
|
193
|
+
boot_to_module_ms: processState.moduleEvalUptimeMs,
|
|
194
|
+
module_to_request_ms: moduleToRequestMs(state),
|
|
178
195
|
framework_ready_wait_ms: state.frameworkReadyWaitMs,
|
|
179
196
|
runtime_provider: runtimeProvider(),
|
|
180
197
|
function_name: envValue("AWS_LAMBDA_FUNCTION_NAME"),
|
|
@@ -238,20 +255,99 @@ function requestTelemetryState(event) {
|
|
|
238
255
|
export function getHttpRequestTelemetryId(event) {
|
|
239
256
|
return requestTelemetryState(event)?.requestId;
|
|
240
257
|
}
|
|
241
|
-
function appendServerTiming(response, event, name, durationMs) {
|
|
258
|
+
function appendServerTiming(response, event, name, durationMs, desc) {
|
|
242
259
|
const duration = Math.max(0, Math.round(durationMs));
|
|
260
|
+
const suffix = desc ? `;desc=${JSON.stringify(desc)}` : "";
|
|
243
261
|
try {
|
|
244
|
-
response.headers.append("server-timing", `${name};dur=${duration}`);
|
|
262
|
+
response.headers.append("server-timing", `${name};dur=${duration}${suffix}`);
|
|
245
263
|
}
|
|
246
264
|
catch {
|
|
247
265
|
try {
|
|
248
|
-
setServerTiming(event, name, { dur: duration });
|
|
266
|
+
setServerTiming(event, name, desc ? { dur: duration, desc } : { dur: duration });
|
|
249
267
|
}
|
|
250
268
|
catch {
|
|
251
269
|
// Some adapters finalize headers eagerly. Tracking still runs.
|
|
252
270
|
}
|
|
253
271
|
}
|
|
254
272
|
}
|
|
273
|
+
/**
|
|
274
|
+
* Does this response get stored in a shared (CDN) cache and replayed?
|
|
275
|
+
*
|
|
276
|
+
* SSR HTML and React Router `.data` are one impersonal shell hard-cached for
|
|
277
|
+
* every visitor, so their headers are written ONCE by the origin render and
|
|
278
|
+
* then handed unchanged to everyone who hits the cache afterwards.
|
|
279
|
+
*/
|
|
280
|
+
function isSharedCacheable(response) {
|
|
281
|
+
const cacheControl = response.headers.get("cache-control")?.toLowerCase() ?? "";
|
|
282
|
+
if (!cacheControl)
|
|
283
|
+
return false;
|
|
284
|
+
if (/\b(?:no-store|no-cache|private)\b/.test(cacheControl))
|
|
285
|
+
return false;
|
|
286
|
+
return (/\bpublic\b/.test(cacheControl) || /\bs-maxage=[1-9]/.test(cacheControl));
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Per-phase `server-timing` entries do NOT belong on a shared-cacheable
|
|
290
|
+
* response. `app;dur=2159` on a cached shell describes one origin render from
|
|
291
|
+
* an arbitrary point in the past, yet every later visitor reads it as the cost
|
|
292
|
+
* of their own request — a stale number wearing a live number's name.
|
|
293
|
+
*
|
|
294
|
+
* So a cacheable response gets exactly one entry, `origin`, whose description
|
|
295
|
+
* leads with the absolute wall-clock time of the render that produced it. Two
|
|
296
|
+
* visitors comparing notes see the identical timestamp, which is what a replay
|
|
297
|
+
* is. The live per-invocation breakdown goes to the slow-request log line and
|
|
298
|
+
* to tracking instead; neither is ever cached.
|
|
299
|
+
*/
|
|
300
|
+
function originSnapshotDesc(state) {
|
|
301
|
+
const parts = [new Date(state.startedAt).toISOString()];
|
|
302
|
+
if (state.requestSequence === 1) {
|
|
303
|
+
parts.push("cold", `boot=${processState.moduleEvalUptimeMs}`, `init=${moduleToRequestMs(state)}`);
|
|
304
|
+
}
|
|
305
|
+
if (state.frameworkReadyWaitMs > 0) {
|
|
306
|
+
parts.push(`startup=${Math.round(state.frameworkReadyWaitMs)}`);
|
|
307
|
+
}
|
|
308
|
+
if (state.db.operationCount > 0) {
|
|
309
|
+
parts.push(`db=${Math.round(state.db.operationWallMs)}`, `dbops=${state.db.operationCount}`);
|
|
310
|
+
}
|
|
311
|
+
return parts.join(" ");
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* One structured line per cold or slow request, straight to stdout.
|
|
315
|
+
*
|
|
316
|
+
* Function logs are the only timing surface readable without a deploy, and
|
|
317
|
+
* `track()` silently no-ops when no tracking provider is registered — which is
|
|
318
|
+
* the common case. Deliberately NOT wrapped in a catch: a swallowed emit would
|
|
319
|
+
* leave a slow request indistinguishable from a fast one.
|
|
320
|
+
*/
|
|
321
|
+
function logSlowRequest(event, state, response, durationMs, pathname) {
|
|
322
|
+
const coldStart = state.requestSequence === 1;
|
|
323
|
+
if (!coldStart && durationMs < SLOW_REQUEST_MS)
|
|
324
|
+
return;
|
|
325
|
+
console.log(JSON.stringify({
|
|
326
|
+
event: SLOW_REQUEST_LOG_EVENT,
|
|
327
|
+
app: getAppName(),
|
|
328
|
+
method: getMethod(event),
|
|
329
|
+
path: normalizeHttpTelemetryPath(pathname),
|
|
330
|
+
status: responseStatusCode(event, response),
|
|
331
|
+
duration_ms: Math.round(durationMs),
|
|
332
|
+
cold_start: coldStart,
|
|
333
|
+
request_sequence: state.requestSequence,
|
|
334
|
+
boot_to_module_ms: processState.moduleEvalUptimeMs,
|
|
335
|
+
module_to_request_ms: moduleToRequestMs(state),
|
|
336
|
+
process_age_ms: state.processAgeAtStartMs,
|
|
337
|
+
framework_ready_wait_ms: Math.round(state.frameworkReadyWaitMs),
|
|
338
|
+
db_ms: Math.round(state.db.operationWallMs),
|
|
339
|
+
db_connect_ms: Math.round(state.db.connectTotalMs),
|
|
340
|
+
db_operation_count: state.db.operationCount,
|
|
341
|
+
db_error_count: state.db.errorCount,
|
|
342
|
+
db_timeout_count: state.db.timeoutCount,
|
|
343
|
+
startup_db_ms: state.startupDb
|
|
344
|
+
? Math.round(state.startupDb.operationWallMs)
|
|
345
|
+
: undefined,
|
|
346
|
+
shared_cacheable: response ? isSharedCacheable(response) : undefined,
|
|
347
|
+
runtime_provider: runtimeProvider(),
|
|
348
|
+
request_id: state.requestId,
|
|
349
|
+
}));
|
|
350
|
+
}
|
|
255
351
|
export function recordFrameworkReadyWait(event, durationMs) {
|
|
256
352
|
const state = requestTelemetryState(event);
|
|
257
353
|
if (state) {
|
|
@@ -295,7 +391,17 @@ export function installHttpResponseTelemetryHooks(nitroApp) {
|
|
|
295
391
|
// Some adapters finalize headers eagerly. Tracking still has the id.
|
|
296
392
|
}
|
|
297
393
|
}
|
|
394
|
+
if (isSharedCacheable(response)) {
|
|
395
|
+
appendServerTiming(response, event, "origin", durationMs, originSnapshotDesc(state));
|
|
396
|
+
logSlowRequest(event, state, response, durationMs, requestPath(event));
|
|
397
|
+
emitTelemetry(event, state, response);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
298
400
|
appendServerTiming(response, event, "app", durationMs);
|
|
401
|
+
if (state.requestSequence === 1) {
|
|
402
|
+
appendServerTiming(response, event, "boot", processState.moduleEvalUptimeMs);
|
|
403
|
+
appendServerTiming(response, event, "init", moduleToRequestMs(state));
|
|
404
|
+
}
|
|
299
405
|
if (state.frameworkReadyWaitMs > 0) {
|
|
300
406
|
appendServerTiming(response, event, "startup", state.frameworkReadyWaitMs);
|
|
301
407
|
}
|
|
@@ -309,6 +415,7 @@ export function installHttpResponseTelemetryHooks(nitroApp) {
|
|
|
309
415
|
appendServerTiming(response, event, "startup-db", state.startupDb.operationWallMs);
|
|
310
416
|
appendServerTiming(response, event, "startup-db-connect", state.startupDb.connectTotalMs);
|
|
311
417
|
}
|
|
418
|
+
logSlowRequest(event, state, response, durationMs, requestPath(event));
|
|
312
419
|
emitTelemetry(event, state, response);
|
|
313
420
|
});
|
|
314
421
|
}
|
|
@@ -2,6 +2,7 @@ import { CONTEXT_XRAY_MIGRATIONS } from "../agent/context-xray/migrations.js";
|
|
|
2
2
|
import { OBSERVATIONAL_MEMORY_MIGRATIONS } from "../agent/observational-memory/migrations.js";
|
|
3
3
|
import { runMigrations } from "../db/migrations.js";
|
|
4
4
|
import { runAutomationRunMigrations } from "../jobs/run-history.js";
|
|
5
|
+
import { runAutomationSchedulerHealthMigrations } from "../jobs/scheduler-health.js";
|
|
5
6
|
import { ORG_MIGRATIONS } from "../org/migrations.js";
|
|
6
7
|
import { runBetterAuthMigrations } from "./better-auth-migrations.js";
|
|
7
8
|
/**
|
|
@@ -21,4 +22,5 @@ export async function runFrameworkReleaseMigrations(nitroApp) {
|
|
|
21
22
|
table: "_observational_memory_migrations",
|
|
22
23
|
})(nitroApp);
|
|
23
24
|
await runAutomationRunMigrations(nitroApp);
|
|
25
|
+
await runAutomationSchedulerHealthMigrations(nitroApp);
|
|
24
26
|
}
|
|
@@ -193,6 +193,14 @@ function isSsrHtmlOrDataResponse(headers, status, pathname) {
|
|
|
193
193
|
* │ hatch — because that is what poisons a shared CDN cache key. A value fixed │
|
|
194
194
|
* │ for the whole deployment cannot. │
|
|
195
195
|
* └──────────────────────────────────────────────────────────────────────────┘
|
|
196
|
+
*
|
|
197
|
+
* The same sharing rule governs any DIAGNOSTIC header on this response. A
|
|
198
|
+
* per-request timing written here is stored once by the origin render and
|
|
199
|
+
* replayed unchanged to every later visitor, so it must not wear a live-looking
|
|
200
|
+
* phase name. `installHttpResponseTelemetryHooks` detects the shared-cacheable
|
|
201
|
+
* policy stamped below and collapses `server-timing` to one `origin` entry
|
|
202
|
+
* carrying the render's wall-clock time; do not add a per-request header here
|
|
203
|
+
* that contradicts that.
|
|
196
204
|
*/
|
|
197
205
|
function applyDefaultSsrCacheHeader(headers, status, pathname) {
|
|
198
206
|
if (!isSsrHtmlOrDataResponse(headers, status, pathname))
|
|
@@ -3,8 +3,10 @@ name: performance
|
|
|
3
3
|
description: >-
|
|
4
4
|
Keep apps and templates loading fast. Read when adding a data model, a
|
|
5
5
|
list/read action, a page or sidebar that loads data, or when something loads
|
|
6
|
-
slowly
|
|
7
|
-
|
|
6
|
+
slowly, or when adding a dependency to the deployed server bundle. Covers
|
|
7
|
+
column projection, indexing hot-path queries, avoiding N+1 and round-trip
|
|
8
|
+
waterfalls, cheap polling, not recomputing on every read, and cold-start
|
|
9
|
+
artifact size.
|
|
8
10
|
scope: dev
|
|
9
11
|
metadata:
|
|
10
12
|
internal: true
|
|
@@ -146,7 +148,9 @@ just one.
|
|
|
146
148
|
|
|
147
149
|
If you're debugging a slow first response, check whether something
|
|
148
150
|
re-personalized the shell before concluding the render itself is slow — the
|
|
149
|
-
fix is client-side data loading after the shell paints, never per-user SSR.
|
|
151
|
+
fix is client-side data loading after the shell paints, never per-user SSR. If
|
|
152
|
+
the shell is clean and a cold miss is still seconds long, the cost is upstream
|
|
153
|
+
of the render: see §9.
|
|
150
154
|
See the `authentication` skill for the full model and `guard:ssr-cache-shell`
|
|
151
155
|
plus `ssr-handler.spec.ts` (`packages/core/src/server/ssr-handler.ts`) for the
|
|
152
156
|
enforced contract.
|
|
@@ -231,6 +235,50 @@ await backfillOneRow(); // guard:allow-boot-data-work — single row, bounded
|
|
|
231
235
|
branch adds. It cannot see everything — a helper that hides the work one call
|
|
232
236
|
deeper reads as innocent — so the rule matters more than the check.
|
|
233
237
|
|
|
238
|
+
## 9. Cold start is the artifact, not just the work it does
|
|
239
|
+
|
|
240
|
+
§8 covers what the process does at boot. This covers how much there is to boot.
|
|
241
|
+
Measured in production: a cold cache miss on `www.agent-native.com` returned in
|
|
242
|
+
**4.5–6.0s** while the in-handler `server-timing: app;dur` was only **~2100ms** —
|
|
243
|
+
the other ~2900ms is platform init, spent before any of our code evaluates. A
|
|
244
|
+
different app with a healthy database measured **13.4s** TTFB on its first cold
|
|
245
|
+
request with `app;dur=1338`, so ~12s of init. Platform init scales with the size
|
|
246
|
+
of the deployed artifact. Every app pays it, and no query tuning can reach it.
|
|
247
|
+
|
|
248
|
+
- **The `/*` page function is the one every visitor's cache miss wakes.** Nothing
|
|
249
|
+
belongs in it that a page render cannot call. Headless browsers, ffmpeg, image
|
|
250
|
+
rasterizers, and other heavy runtimes belong in the function that actually
|
|
251
|
+
invokes them, or behind a job — not in the default handler. PR #2684, titled
|
|
252
|
+
"Harden auth and cold-start data paths", put 78MB of headless Chromium into
|
|
253
|
+
every page function; nothing in the diff looked like a performance change.
|
|
254
|
+
- **Each extra emitted function is a full second copy of the bundle.** Netlify
|
|
255
|
+
copies the whole server directory per function, so splitting out a
|
|
256
|
+
`-background` or per-route function multiplies existing weight rather than
|
|
257
|
+
dividing it. Trim the artifact before you split it.
|
|
258
|
+
- **Already-compressed binaries do not shrink again in the deploy zip.** A
|
|
259
|
+
Brotli-packed browser or a static ffmpeg build costs close to its full size in
|
|
260
|
+
upload and in cold-start extraction. Budget from bytes on disk, never from an
|
|
261
|
+
assumption that compression will absorb it.
|
|
262
|
+
- **Never resolve a copied dependency by walking ancestor `node_modules`.** In a
|
|
263
|
+
monorepo that walk does not fail — it finds a sibling app's copy and ships
|
|
264
|
+
that. Resolve from the app's own dependency root and throw when it is missing;
|
|
265
|
+
a silently-found wrong package is precisely the indistinguishable-from-success
|
|
266
|
+
failure this repo bans.
|
|
267
|
+
|
|
268
|
+
`packages/core/src/deploy/build.ts` is what decides all of this: the
|
|
269
|
+
platform/arch filter at `:2384-2410` and the per-preset copy list at
|
|
270
|
+
`:4499-4504`. Adding a package there adds it to every page function.
|
|
271
|
+
|
|
272
|
+
**Measure a built bundle by timing the import and forcing exit.** Never measure
|
|
273
|
+
by waiting for the process to exit — module scope starts timers and opens
|
|
274
|
+
handles, so process lifetime measures those, not boot cost. That exact mistake
|
|
275
|
+
produced a wrong number during the investigation behind this section.
|
|
276
|
+
|
|
277
|
+
```sh
|
|
278
|
+
node -e 'const t=Date.now();import(process.argv[1]).then(()=>{console.log(`${Date.now()-t}ms`);process.exit(0)})' \
|
|
279
|
+
./.netlify/functions-internal/server/main.mjs
|
|
280
|
+
```
|
|
281
|
+
|
|
234
282
|
## Checklist — run before shipping a list/read or a new table
|
|
235
283
|
|
|
236
284
|
- [ ] List selects only displayed columns; heavy blobs excluded or `substr`-truncated.
|
|
@@ -246,3 +294,6 @@ deeper reads as innocent — so the rule matters more than the check.
|
|
|
246
294
|
aggregations and re-syncs run on every cold start there (see §8).
|
|
247
295
|
- [ ] Mutation-fresh reads go through actions + `useActionQuery`, not SSR loader
|
|
248
296
|
data.
|
|
297
|
+
- [ ] No heavy runtime (browser, ffmpeg, rasterizer) added to what the `/*` page
|
|
298
|
+
function ships, and no new copied dependency resolved by walking ancestor
|
|
299
|
+
`node_modules` (see §9).
|
package/dist/vite/client.js
CHANGED
|
@@ -2547,7 +2547,11 @@ const DEFAULT_VITE_WATCH_IGNORES = [
|
|
|
2547
2547
|
function forceServeOnly(pluginOrPreset) {
|
|
2548
2548
|
if (Array.isArray(pluginOrPreset))
|
|
2549
2549
|
return pluginOrPreset.map(forceServeOnly);
|
|
2550
|
-
return {
|
|
2550
|
+
return {
|
|
2551
|
+
...pluginOrPreset,
|
|
2552
|
+
apply: (_config, configEnv) => configEnv.command === "serve" &&
|
|
2553
|
+
!(configEnv.isPreview && process.env.IS_RR_BUILD_REQUEST === "yes"),
|
|
2554
|
+
};
|
|
2551
2555
|
}
|
|
2552
2556
|
function nitroPresetMarkerPlugin(options) {
|
|
2553
2557
|
const preset = options.nitro?.preset;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.146.
|
|
3
|
+
"version": "0.146.7",
|
|
4
4
|
"description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -3,8 +3,10 @@ name: performance
|
|
|
3
3
|
description: >-
|
|
4
4
|
Keep apps and templates loading fast. Read when adding a data model, a
|
|
5
5
|
list/read action, a page or sidebar that loads data, or when something loads
|
|
6
|
-
slowly
|
|
7
|
-
|
|
6
|
+
slowly, or when adding a dependency to the deployed server bundle. Covers
|
|
7
|
+
column projection, indexing hot-path queries, avoiding N+1 and round-trip
|
|
8
|
+
waterfalls, cheap polling, not recomputing on every read, and cold-start
|
|
9
|
+
artifact size.
|
|
8
10
|
scope: dev
|
|
9
11
|
metadata:
|
|
10
12
|
internal: true
|
|
@@ -146,7 +148,9 @@ just one.
|
|
|
146
148
|
|
|
147
149
|
If you're debugging a slow first response, check whether something
|
|
148
150
|
re-personalized the shell before concluding the render itself is slow — the
|
|
149
|
-
fix is client-side data loading after the shell paints, never per-user SSR.
|
|
151
|
+
fix is client-side data loading after the shell paints, never per-user SSR. If
|
|
152
|
+
the shell is clean and a cold miss is still seconds long, the cost is upstream
|
|
153
|
+
of the render: see §9.
|
|
150
154
|
See the `authentication` skill for the full model and `guard:ssr-cache-shell`
|
|
151
155
|
plus `ssr-handler.spec.ts` (`packages/core/src/server/ssr-handler.ts`) for the
|
|
152
156
|
enforced contract.
|
|
@@ -231,6 +235,50 @@ await backfillOneRow(); // guard:allow-boot-data-work — single row, bounded
|
|
|
231
235
|
branch adds. It cannot see everything — a helper that hides the work one call
|
|
232
236
|
deeper reads as innocent — so the rule matters more than the check.
|
|
233
237
|
|
|
238
|
+
## 9. Cold start is the artifact, not just the work it does
|
|
239
|
+
|
|
240
|
+
§8 covers what the process does at boot. This covers how much there is to boot.
|
|
241
|
+
Measured in production: a cold cache miss on `www.agent-native.com` returned in
|
|
242
|
+
**4.5–6.0s** while the in-handler `server-timing: app;dur` was only **~2100ms** —
|
|
243
|
+
the other ~2900ms is platform init, spent before any of our code evaluates. A
|
|
244
|
+
different app with a healthy database measured **13.4s** TTFB on its first cold
|
|
245
|
+
request with `app;dur=1338`, so ~12s of init. Platform init scales with the size
|
|
246
|
+
of the deployed artifact. Every app pays it, and no query tuning can reach it.
|
|
247
|
+
|
|
248
|
+
- **The `/*` page function is the one every visitor's cache miss wakes.** Nothing
|
|
249
|
+
belongs in it that a page render cannot call. Headless browsers, ffmpeg, image
|
|
250
|
+
rasterizers, and other heavy runtimes belong in the function that actually
|
|
251
|
+
invokes them, or behind a job — not in the default handler. PR #2684, titled
|
|
252
|
+
"Harden auth and cold-start data paths", put 78MB of headless Chromium into
|
|
253
|
+
every page function; nothing in the diff looked like a performance change.
|
|
254
|
+
- **Each extra emitted function is a full second copy of the bundle.** Netlify
|
|
255
|
+
copies the whole server directory per function, so splitting out a
|
|
256
|
+
`-background` or per-route function multiplies existing weight rather than
|
|
257
|
+
dividing it. Trim the artifact before you split it.
|
|
258
|
+
- **Already-compressed binaries do not shrink again in the deploy zip.** A
|
|
259
|
+
Brotli-packed browser or a static ffmpeg build costs close to its full size in
|
|
260
|
+
upload and in cold-start extraction. Budget from bytes on disk, never from an
|
|
261
|
+
assumption that compression will absorb it.
|
|
262
|
+
- **Never resolve a copied dependency by walking ancestor `node_modules`.** In a
|
|
263
|
+
monorepo that walk does not fail — it finds a sibling app's copy and ships
|
|
264
|
+
that. Resolve from the app's own dependency root and throw when it is missing;
|
|
265
|
+
a silently-found wrong package is precisely the indistinguishable-from-success
|
|
266
|
+
failure this repo bans.
|
|
267
|
+
|
|
268
|
+
`packages/core/src/deploy/build.ts` is what decides all of this: the
|
|
269
|
+
platform/arch filter at `:2384-2410` and the per-preset copy list at
|
|
270
|
+
`:4499-4504`. Adding a package there adds it to every page function.
|
|
271
|
+
|
|
272
|
+
**Measure a built bundle by timing the import and forcing exit.** Never measure
|
|
273
|
+
by waiting for the process to exit — module scope starts timers and opens
|
|
274
|
+
handles, so process lifetime measures those, not boot cost. That exact mistake
|
|
275
|
+
produced a wrong number during the investigation behind this section.
|
|
276
|
+
|
|
277
|
+
```sh
|
|
278
|
+
node -e 'const t=Date.now();import(process.argv[1]).then(()=>{console.log(`${Date.now()-t}ms`);process.exit(0)})' \
|
|
279
|
+
./.netlify/functions-internal/server/main.mjs
|
|
280
|
+
```
|
|
281
|
+
|
|
234
282
|
## Checklist — run before shipping a list/read or a new table
|
|
235
283
|
|
|
236
284
|
- [ ] List selects only displayed columns; heavy blobs excluded or `substr`-truncated.
|
|
@@ -246,3 +294,6 @@ deeper reads as innocent — so the rule matters more than the check.
|
|
|
246
294
|
aggregations and re-syncs run on every cold start there (see §8).
|
|
247
295
|
- [ ] Mutation-fresh reads go through actions + `useActionQuery`, not SSR loader
|
|
248
296
|
data.
|
|
297
|
+
- [ ] No heavy runtime (browser, ffmpeg, rasterizer) added to what the `/*` page
|
|
298
|
+
function ships, and no new copied dependency resolved by walking ancestor
|
|
299
|
+
`node_modules` (see §9).
|