|
1 | 1 | # Performance |
2 | 2 |
|
3 | | -Failures: Firefox + Mobile Safari FID measured 127 ms/126 ms (threshold 100 ms) and Chrome/Edge/Mobile Chrome TBT measured 297 ms-511 ms (threshold 200 ms). |
| 3 | +Latest run (Dec 2, 2025): |
| 4 | + |
| 5 | +- Mobile Chrome LCP measured 4.3 s (threshold 2.5 s) |
| 6 | +- Mobile Chrome FCP measured 5.3 s (threshold 1.8 s) |
| 7 | +- Mobile Chrome TBT measured 228 ms (threshold 200 ms) |
| 8 | +- Microsoft Edge TBT measured 466 ms (threshold 200 ms) |
| 9 | +- Historical: Firefox + Mobile Safari FID measured 127 ms/126 ms (threshold 100 ms) |
| 10 | + |
| 11 | +## Largest Contentful Paint regressions (Mobile Chrome) |
| 12 | + |
| 13 | +- `src/components/Animations/Computers/index.astro` injects a 600×600 SVG and immediately executes `@components/Animations/Computers/client/index.ts`, which imports GSAP, Lit, and the animation store. The custom element performs >150 `gsap.set`/`gsap.to` calls synchronously before setting the SVG to `visibility: visible`, so the hero (the LCP candidate) is timestamped around 4.2 s on the Pixel emulation. |
| 14 | + |
| 15 | +- `astro.config.ts` enables `prefetch: true`, so the client router prefetches every in-viewport `<a>` after `DOMContentLoaded`. The home page exposes 30+ internal links (hero CTAs, three carousels, CTA blocks, footer), meaning the hero image request is queued behind dozens of route/module fetches, delaying paint. |
| 16 | + |
| 17 | +- Playwright perf runs disable the service worker (`window.__disableServiceWorkerForE2E = true`), so every test is a cold navigation that redownloads fonts and hero artwork. When that happens concurrently with GSAP initialization, the hero frequently lands outside the 2.5 s budget. |
| 18 | + |
| 19 | +**Mitigations:** |
| 20 | + |
| 21 | +- Convert the hero animation into a lazy island (e.g., `client:visible` + `requestIdleCallback`) so GSAP only loads after the hero intersects, and ship a static SVG poster by default. |
| 22 | + |
| 23 | +- Gate global prefetching behind an env flag (such as `prefetch: process.env.DISABLE_PREFETCH === '1' ? false : true`) so CI/perf runs can keep hero requests at the front of the queue. |
| 24 | + |
| 25 | +- Provide a cached hero path for tests (preload hero assets once per Playwright worker or expose a hook that skips hero hydration) so each navigation is not a cold start. |
| 26 | + |
| 27 | +## First Contentful Paint regressions (Mobile Chrome) |
| 28 | + |
| 29 | +- `BaseLayout.astro` registers CookieConsent, ThemePicker, Header/Nav, and the hero animation before `<main>`. Each registration pulls in `lit`, `embla`, `gsap`, and `@components/scripts/store`, so the browser spends several seconds parsing/executing JS before the first paint can occur, pushing FCP to 5.3 s. |
| 30 | + |
| 31 | +- The cookie banner (`@components/Consent/Banner/index.astro`) renders a full-width dialog at the top of the DOM. Even though `BasePage.dismissCookieModal()` hides it later, the overlay participates in the first paint and forces layout work for a large chunk of HTML before hero text can display. |
| 32 | + |
| 33 | +- Because service workers are disabled, every perf test re-fetches fonts, newsletter assets, and carousel data. Coupled with immediate hydration of three carousels plus the hero animation, the browser cannot emit `first-contentful-paint` until those synchronous tasks finish. |
| 34 | + |
| 35 | +**Mitigations:** |
| 36 | + |
| 37 | +- Move heavy islands (hero animation, carousels, testimonials) to `client:visible`/`client:idle` and fall back to simple CSS transitions for the above-the-fold hero so HTML can paint immediately. |
| 38 | + |
| 39 | +- Defer cookie banner hydration for perf/E2E contexts (e.g., check `window.isPlaywrightControlled` or wait for user interaction) so it does not participate in the first paint. |
| 40 | + |
| 41 | +- Introduce a perf-test guard (`if (window.__E2E_DISABLE_HEAVY_JS) return`) inside each `register*WebComponent` helper so Playwright runs can opt out of hydrating non-critical animations entirely. |
4 | 42 |
|
5 | 43 | ## First Input Delay (FID) regressions (Firefox, Mobile Safari) |
6 | 44 |
|
7 | | -- measureFID clicks the first interactive container right after networkidle, so any document-level listener that executes synchronously inflates this metric. index.ts binds multiple global click, touchend, and keydown handlers plus Lit store updates during initialize(). Those handlers always run before the synthetic click resolves, keeping the main thread busy for ~25 ms on desktop and ~40 ms on mobile. |
| 45 | +- `src/components/Navigation/client/index.ts` click interceptors and Lit store updates run as soon as `measureFID` triggers a click after `networkidle`. Those document-level handlers run before the synthetic click completes, keeping the main thread busy for ~25 ms on desktop and ~40 ms on mobile. |
8 | 46 |
|
9 | | -- index.ts eagerly instantiates focus-trap, registers document keyup/pointerdown, and intercepts every nav link to reroute through astro:transitions. All of that work executes before the first user click completes, which is why only browsers with slower event loop scheduling (Gecko/WebKit) exceed 100 ms. |
| 47 | +- The navigation script eagerly instantiates focus-trap helpers, registers document `keyup`/`pointerdown`, and reroutes every `<a>` through Astro view transitions. Browsers with slower event-loop scheduling (Gecko/WebKit) consistently exceed 100 ms because all of that work executes before the first input resolves. |
10 | 48 |
|
11 | 49 | **Mitigations:** |
12 | 50 |
|
13 | | -- lazily register these web components behind requestIdleCallback/queueMicrotask so initial navigation/interactions aren't blocked |
14 | | -- bind document listeners only after the relevant UI opens |
15 | | -- gate LitElement hydration on an IntersectionObserver so off-screen modals (theme picker) stay inert until revealed. |
| 51 | +- Lazily register nav/theme components behind `requestIdleCallback`/`queueMicrotask` so first interactions are not blocked. |
| 52 | + |
| 53 | +- Bind document listeners only after the relevant UI opens instead of during global initialization. |
| 54 | + |
| 55 | +- Gate LitElement hydration on an `IntersectionObserver` so off-screen modals stay inert until revealed. |
16 | 56 |
|
17 | 57 | ## Total Blocking Time (TBT) regressions (Chromium desktop + mobile) |
18 | 58 |
|
19 | | -- Carousels (index.ts and index.ts) synchronously instantiate Embla + Autoplay during DOMContentLoaded, even when the slider is below the fold. Embla's layout sync plus dot/button building accounts for ~180 ms of long tasks per carousel; Mobile Chrome shows 500 ms because hero and testimonial sliders initialize back-to-back. |
| 59 | +- Every carousel (`src/components/Carousel/client/index.ts`) hydrates on DOMContentLoaded, instantiating Embla + Autoplay even when sliders sit below the fold. Each instance builds navigation buttons, dots, and animation controllers synchronously, adding 180–220 ms of blocking time on Mobile Chrome and >450 ms on Edge when three carousels initialize back-to-back. |
20 | 60 |
|
21 | | -- Calling createAnimationController inside every carousel triggers initAnimationLifecycle (animationLifecycle.ts) immediately, which sets up persistent atoms, matchMedia('(prefers-reduced-motion)'), several document/window listeners, and writes to localStorage. This runs regardless of whether animations are visible, adding another ~80 ms blocking chunk. |
| 61 | +- `createAnimationController` registers nanostore observers, `matchMedia('(prefers-reduced-motion)')`, and localStorage writes for every animation (hero + each carousel). Those listeners run before the page becomes interactive and stack up inside the 5 s TBT window. |
22 | 62 |
|
23 | | -- Global error-handling utilities (@components/scripts/errors/*) are imported by all of the above and run in the same navigation task. Whenever addScriptBreadcrumb fires during initialization, it formats stack traces and sends data to the client logger, increasing long-task duration. |
| 63 | +- Hero GSAP initialization plus consent logging (`initConsentSideEffects` immediately fetches `/api/gdpr/consent`) execute in the same navigation task, so the long-task observer inside `measureTBT` still sees 200–400 ms of blocking work even after we wait for `networkidle`. |
24 | 64 |
|
25 | 65 | **Mitigations** |
26 | 66 |
|
27 | | -- Convert carousel/testimonial scripts to lazy web components that hydrate only when the element enters the viewport, and defer Embla imports via dynamic import() to split the bundle. |
| 67 | +- Lazily hydrate carousels/testimonials (intersection observer + dynamic `import('embla-carousel')`) and skip Autoplay during CI/perf runs so fewer long tasks fall inside the measurement window. |
28 | 68 |
|
29 | | -- Gate animation lifecycle initialization until the first component actually requests animation control, and wrap it in requestIdleCallback so it never runs inside the navigation task. |
| 69 | +- Only create animation controllers once an animation actually needs lifecycle hooks, and wrap registration in `requestIdleCallback` so it lands outside the navigation task. |
30 | 70 |
|
31 | | -- Move breadcrumb/error instrumentation behind an environment flag or lazy loader so production visitors don't pay the cost before interaction. |
| 71 | +- Guard consent logging and hero animation hydration with `window.isPlaywrightControlled` so Playwright perf tests can bypass those fetches and GSAP timelines when only static markup is required. |
32 | 72 |
|
33 | 73 | ## Next steps |
34 | 74 |
|
35 | | -- Capture a Chrome DevTools performance trace on / with CPU throttling to confirm which long tasks map to Embla initialization, ThemePicker, Navigation, and animation lifecycle. |
| 75 | +- Capture a CPU-throttled Chrome trace on `/` with and without prefetching to quantify how much GSAP + Embla delay LCP/FCP. |
| 76 | + |
| 77 | +- Add environment toggles for prefetching, hero animation hydration, and consent logging so performance tests can disable those features deterministically. |
36 | 78 |
|
37 | | -- Implement lazy hydration/dynamic imports for ThemePicker, Navigation, and Carousel components, then rerun core-web-vitals.spec.ts to verify FID/TBT drop below thresholds. |
| 79 | +- Implement lazy hydration/dynamic imports for ThemePicker, Navigation, Carousel, and hero animation components, then rerun `core-web-vitals.spec.ts` across all browser profiles to verify the improvements. |
0 commit comments