Skip to main content

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:

ValueSourceCost per request
configprocess.env (same fields as /api/env)none
flagsbackend /FeatureFlags, memoised 60 s per instanceone backend call per minute
sessiongetServerSession(authOptions) — a JWT cookie decodenone (no Cosmos, no token refresh)
arcgisTokenlib/server/arcgisToken.ts, cached until 10 min before expiryone 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 the authenticated / unauthenticated state immediately instead of loading, so AuthGuard never shows its spinner on a warm session and /api/auth/session is 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/env and /api/feature-flags exactly as before.
  • queryClient.setQueryData(QUERY_KEYS.ARCGIS_TOKEN, …) in RootLayoutClient — the map's useArcGISAuth() 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-static and /api/camera-static/[cameraId] compress only when the request carries the session cookie. That is precisely the condition under which Front Door's cacheBypassOnNextAuth rule 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: br and a content-length in 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/session or /api/arcgis/token request before the camera list renders.