# The consent banner paints from the HTML, repeat visits skip revalidation, and real Web Vitals

First visits to sign-in and the app reach their largest paint 2 to 3 seconds sooner, repeat visits hydrate up to half a second sooner, the landing figure is sent once, the dashboard's hydration error is gone, and visitors who allow analytics report Core Web Vitals.

Six changes from the x.com rebuild research, each measured against `main` on the slow profile (Slow 4G, 4x CPU, the built Worker, median of 5). Method and every number: [`docs/measurements/2026-11-06-web-quick-wins.md`](https://github.com/CarlosZiegler/varu/blob/main/docs/measurements/2026-11-06-web-quick-wins.md).

- **The cookie banner is in the server HTML.** It used to mount after hydration and was the largest paint of a first visit to `/login`, `/dashboard` and `/todos`. The choice now lives in a `cookie_consent` cookie the server reads, so the banner paints with the page: largest paint 2.52 → 0.50 s on `/login`, 3.54 → 1.00 s on `/dashboard`, 4.02 → 0.99 s on `/todos`. Choices stored in the browser before this release are moved to the cookie without the banner flashing. Categories, defaults and the analytics gate are unchanged. ([0406d2f](https://github.com/CarlosZiegler/varu/commit/0406d2fdb09a0d9dbd810ec440972bf3d033a4db))
- **Hashed assets are cached for a year.** `/assets/*` answers `public, max-age=31536000, immutable` (`apps/web/public/_headers`; the build fails if an unhashed file lands there). A repeat visit takes scripts from the cache without a request: the landing hydrates at 0.92 s instead of 1.41 s, `/dashboard` paints at 0.20 s instead of 0.40 s and hydrates at 0.50 s instead of 1.01 s. Favicons, figures and other public files keep revalidating. ([ee97233](https://github.com/CarlosZiegler/varu/commit/ee9723356bc4caaa39f8ccaea439b4ac895e5543))
- **The landing figure is sent once.** The request machine was inlined in the HTML and shipped again as 130 KB of JSON in the landing script. The server now inlines the generated SVG and hydration keeps it; the landing chunk drops from 57 to 33 KB gzip and a first visit hydrates 0.28 s sooner. The drawing and its animation are pixel-identical. ([dd62551](https://github.com/CarlosZiegler/varu/commit/dd62551c194cc90195bd26e779517fedc6023dad))
- **No hydration error on the dashboard.** Relative times ("5 minutes ago") were computed from the clock at the server render and again at hydration, seconds later, and React threw #418 whenever the rounding changed in between. `useFormatTimeAgo` counts from the moment the server rendered until hydration; the activity timeline's day headings now use the visitor's time zone as well ([W16](/docs/development/web-conventions)). ([772bef0](https://github.com/CarlosZiegler/varu/commit/772bef0a0121e368999281556b37eaee4c90b9c1))
- **Web Vitals from real visits.** Visitors who allow analytics in the banner report LCP, INP, CLS, FCP and TTFB with the route pattern (never the URL), language, navigation type and connection class to `POST /api/events/web-vitals`, which writes one `web_vital` line to Workers Logs. Nothing is observed or sent without that consent, and nothing loads before the first paint. See [Observability](/docs/architecture/observability). ([bcfbd84](https://github.com/CarlosZiegler/varu/commit/bcfbd841de597eb0025aef224afd9e6f898ec5df))
- **The Workers Logs sampling rate is explicit.** `observability.logs.head_sampling_rate` is set to `1` in `wrangler.jsonc`, with what lowering it saves and loses (sampling drops whole requests, errors included) and the cost table in [Observability](/docs/architecture/observability). ([67588c2](https://github.com/CarlosZiegler/varu/commit/67588c2f32cca382a5c601f2729925e5a54c29a3))

