Startup Performance
Three decisions shape how fast the app becomes usable. Each one exists because of a measurement, and each one has a failure mode worth knowing before you change it.
1. The root layout resolves config, flags and the session on the server
app/layout.tsx is a server component. Per request it calls getAppBootstrap()
(lib/server/appBootstrap.ts), which resolves:
| Value | Source | Cost per request |
|---|---|---|
config | process.env (same fields as /api/env) | none |
flags | backend /FeatureFlags, memoised 60 s per instance | one backend call per minute |
session | getServerSession(authOptions) — a JWT cookie decode | none (no Cosmos, no token refresh) |
arcgisToken | lib/server/arcgisToken.ts, cached until 10 min before expiry | one ArcGIS OAuth call per ~12 h |
The result is passed as the bootstrap prop to app/RootLayoutClient.tsx, which is the
client component that renders <html>, <body> and every provider exactly as the old
client-only layout did. Three things consume it:
SessionProvider session={bootstrap.session}— next-auth starts in theauthenticated/unauthenticatedstate immediately instead ofloading, soAuthGuardnever shows its spinner on a warm session and/api/auth/sessionis not requested on load.ConfigProvider initialConfig initialFlags— the stores are filled in an effect and the loader is skipped. When rendered without these props (Storybook, tests, mock mode) the provider still fetches/api/envand/api/feature-flagsexactly as before.queryClient.setQueryData(QUERY_KEYS.ARCGIS_TOKEN, …)inRootLayoutClient— the map'suseArcGISAuth()resolves synchronously and<arcgis-map>mounts on first render.
The API routes still exist and share the same helpers, so a stale client bundle or a mock
run keeps working. Keep getPublicConfig() and /api/env in sync when adding a field.
2. JSON responses are compressed at the origin
Azure Front Door only compresses responses on routes with edge caching enabled. Every
dynamic /api/* route is on the no-cache bypass route, and next.config.mjs has
compress: false, so until this change every polled body crossed the wire raw — the
all-cameras operational payload is ~700 KB uncompressed and ~140 KB as brotli, a 6 h timelapse
~640 KB vs ~47 KB.
withApiHandler now calls installCompressedJson(req, res) (lib/compressResponse.ts),
which replaces res.json() for that request with a version that brotli/gzip-encodes the
body when the client sent Accept-Encoding and the body is over 1 KB. The full body is in
memory, so the response always carries an exact Content-Length — the reason
compress: true was rejected is that Next's streaming compressor chunk-encodes without a
length, which produced ERR_HTTP2_PROTOCOL_ERROR behind Front Door in May 2026.
The two edge-cached routes are handled explicitly:
/api/cameras-all-staticand/api/camera-static/[cameraId]compress only when the request carries the session cookie. That is precisely the condition under which Front Door'scacheBypassOnNextAuthrule skips its cache (and therefore its own compression). Anonymous responses stay byte-identical so the edge keeps caching and compressing them.
installCompressedJson is a no-op when there is no Accept-Encoding header, which is why the
API route unit tests (which build a bare res mock) are unaffected.
3. ArcGIS stays out of the first paint
The map runtime is ~1 MB of brotli and was reachable from the root page through plain
imports: usePageUpdates (Basemap/GroupLayer/Map), data/basemaps.ts, useFeatureMapAGOLLayers
(WebMap) and Navbar → AlertsPanel → AlertAccordionItem (Point). Those are now type-only
imports or next/dynamic boundaries (AlertsPanel, AlertMessage), so the only thing that
loads ArcGIS is FeatureMap itself, which MapPortalContext already loads lazily.
The ArcGIS theme stylesheet is imported by FeatureMap and MapPolygonFL from
@arcgis/core/assets/esri/themes/light/main.css — bundled, version-matched to the installed
SDK and cached at the edge — instead of the render-blocking js.arcgis.com/4.33/... link that
used to sit in <head> (the installed SDK is 5.x).
Checking it
curl -s -D - -H 'Accept-Encoding: br' -X QUERY -d '{}' https://<host>/api/cameras-all-operational→content-encoding: brand acontent-lengthin the low hundreds of KB.- View source of
/: the RSC payload contains"flags","session"and"arcgisToken". - Network tab on a cold load: no
/api/env,/api/feature-flags,/api/auth/sessionor/api/arcgis/tokenrequest before the camera list renders.