Send client telemetry via sendBeacon to drop it off the critical path#29
Merged
Conversation
The deferral in #28 cut telemetry JS but left the App Insights /v2.1/track POST on the critical request chain (max critical path latency ~2.35s). Lighthouse classifies a request as critical mainly by priority, and a plain fetch() defaults to High — so delaying *when* it fires only moved the node later in the timeline, it didn't remove it. Switch the transport to navigator.sendBeacon, a background-priority transport Lighthouse does not count as critical, with a low-priority keepalive fetch fallback. Beacon also reliably survives page unload, matching the existing visibilitychange/pagehide flush. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
decipher-ms | 351a6f3 | Commit Preview URL Branch Preview URL |
May 29 2026, 05:06 PM |
pedropaulovc
added a commit
that referenced
this pull request
May 29, 2026
## Why Follow-up to #28/#29. A mobile PageSpeed run reported **LCP 5.1 s**, yet local Lighthouse with the *identical* throttling (Moto G Power, Slow 4G, 4× CPU) scored **100 / LCP 1.5 s**, and real TTFB is 40–170 ms. The gap is environmental: the content pages were **SSR on a Worker**, so a slow/cold root-document response from a distant test location cascades (HTML late → fonts late → Playfair lands ~5 s → heading LCP 5.1 s). And for a content site, the client JS was carrying weight it didn't need. ## Changes **1. Prerender content pages** (`index, about, work, briefs/*, legal, privacy, 404`) → static HTML served from the CDN edge, no per-request Worker render, no cold-start TTFB variance. API + OIDC routes stay SSR. To decouple from SSR, **client telemetry self-configures**: the App Insights iKey + ingestion endpoint (already public — they ship in every page today) move to a shared `telemetry-config.ts`; `environment` is resolved from the hostname on each side. The connection-string secret parse and the `__telemetryConfig` injection are removed. `build.format: "file"` so pages emit as `about.html` (served at `/about`) rather than `about/index.html` (which 307-redirects `/about → /about/`) — **keeps URLs identical** to the existing canonical/sitemap/nav scheme, no redirect hop. **2. Drop client-side zod.** The server validation (`server/briefing.ts`) is hand-rolled, not zod — so the ~13 KiB client zod bundle was pure duplication. Replaced with a small `validate()` mirroring the same rules. With zod gone, the form script falls under Astro's inline threshold and is **inlined into the page (one fewer request)**. zod removed from deps. ## Net effect on client JS (a content site now ships analytics-only JS) | | Before | After | |---|---|---| | Form (`BriefingForm`) | 13.79 KiB gz request | **inlined, 0 requests** | | Telemetry | 6.37 KiB, deferred (idle, beacon) | unchanged | | Content page TTFB | per-request Worker (variable) | **static edge (consistent)** | ## Verification (local `wrangler preview`) - All content routes → **200, no redirect**; `/about`, `/work`, etc. served directly - SSR routes intact: `/sitemap.xml`, `/llms.txt`, `/.well-known/openid-configuration` → 200 - Telemetry **beacon fires on the static page** (self-config works without SSR injection) - Form validation: empty → 5 correct per-field errors (role optional); valid → clears - `npm run build` + `eslint` clean > Note: `/.well-known/jwks.json` 500s on local preview (missing `OIDC_PRIVATE_KEY` secret locally); live prod is 200 and this PR touches no OIDC code. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
#28 lazy-loaded the telemetry chunk, but a fresh mobile PageSpeed run on the deployed code showed
/v2.1/trackstill topped the critical request chain:Lighthouse classifies a request as critical primarily by priority, and a plain
fetch()defaults to High. Deferring when it fired (viarequestIdleCallback) only pushed the node later in the timeline — it didn't remove it.Fix
Switch the ingestion transport to
navigator.sendBeacon— a background-priority transport Lighthouse does not count as critical — with a low-priority keepalivefetchfallback for browsers without it. Beacon also reliably survives page unload, which is exactly what the existingvisibilitychange/pagehideflush relies on.Expected result:
/trackleaves the chain entirely, dropping max critical path latency from ~2.35 s to ~0.5 s (next node is the deferred telemetry chunk).🤖 Generated with Claude Code