Skip to content

Send client telemetry via sendBeacon to drop it off the critical path#29

Merged
pedropaulovc merged 1 commit into
mainfrom
perf/telemetry-sendbeacon
May 29, 2026
Merged

Send client telemetry via sendBeacon to drop it off the critical path#29
pedropaulovc merged 1 commit into
mainfrom
perf/telemetry-sendbeacon

Conversation

@pedropaulovc

Copy link
Copy Markdown
Contributor

Problem

#28 lazy-loaded the telemetry chunk, but a fresh mobile PageSpeed run on the deployed code showed /v2.1/track still topped the critical request chain:

Maximum critical path latency: 2,353 ms
https://decipher.ms       - 309 ms
BriefingForm…js           - 393 ms, 13.81 KiB
Layout…Ct5Qvt6k.js        - 399 ms,  1.43 KiB   ← #28 shrank this ✓
telemetry…js              - 504 ms,  6.31 KiB
/v2.1/track               - 2,353 ms            ← still the tail

Lighthouse classifies a request as critical primarily by priority, and a plain fetch() defaults to High. Deferring when it fired (via requestIdleCallback) 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 keepalive fetch fallback for browsers without it. Beacon also reliably survives page unload, which is exactly what the existing visibilitychange/pagehide flush relies on.

Expected result: /track leaves 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

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>
Copilot AI review requested due to automatic review settings May 29, 2026 17:04
@pedropaulovc
pedropaulovc enabled auto-merge May 29, 2026 17:04
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented May 29, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

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 pedropaulovc left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@pedropaulovc
pedropaulovc merged commit a2dbaaa into main May 29, 2026
2 of 3 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants