Mobile PageSpeed Optimisation Results — 2026-05-14
Baseline (from .superpowers/brainstorm/odontox-mobile-pagespeed-report.md)
Tested via Uptrends free tool on Apple iPhone SE / Chrome 148, native speed (unthrottled), Singapore-2 origin.
| Metric | Value |
|---|---|
| Performance score | 49 / 100 |
| FCP | 2,106 ms |
| LCP | 3,853 ms |
| TBT | 198 ms |
| CLS | 0 |
| TTI | 2,499 ms |
| Load time | 6.9 s |
| Page size | ~18 MB (Object payload alone: 13 MB) |
| Total requests | 78 |
Post-Optimisation (2026-05-14)
Tested via Lighthouse CLI v12 (npx -y lighthouse@12) against https://odontox.io/.
Important — throttling note: Lighthouse --form-factor=mobile --throttling-method=simulate applies a
simulated Moto G Power profile: 4G (1.6 Mbps down / 150 ms RTT), 4× CPU slowdown. This is intentionally
more aggressive than the baseline Uptrends “native speed” test; the scores are not directly comparable
on an absolute basis. Use the delta against the page-weight and request-count figures (which are
throttling-independent) and the desktop score as complementary signals.
Mobile (form-factor=mobile, throttling=simulate, Lighthouse v12)
| Metric | Value |
|---|---|
| Performance score | 27 / 100 |
| FCP | 7.0 s |
| LCP | 19.3 s |
| TBT | 2,156 ms |
| CLS | 0 |
| Speed Index | 13.5 s |
| TTI | 19.3 s |
| Total bytes transferred | 6,722 KiB (~6.6 MB) |
| Network requests | 79 |
Desktop (preset=desktop, Lighthouse v12)
| Metric | Value |
|---|---|
| Performance score | 69 / 100 |
| FCP | 1.3 s |
| LCP | 2.7 s |
| TBT | 190 ms |
| CLS | 0 |
| Speed Index | 3.1 s |
| Total bytes transferred | 6,779 KiB (~6.6 MB) |
| Network requests | 80 |
Delta (page-weight and requests — throttling-independent)
| Metric | Baseline | Post-optimisation | Delta |
|---|---|---|---|
| Page size | ~18 MB | ~6.6 MB | -63% (-11.4 MB) |
| Requests | 78 | 79 | ~flat (+1, noise) |
| Desktop LCP | n/a | 2.7 s | — |
| Desktop Performance | n/a | 69 / 100 | — |
The request count is effectively unchanged because the eight tasks were focused on byte weight (kill dead assets, lazy-load media, defer analytics) rather than request count. The 11.4 MB saving comes from Task 5 (11 MBruby.pngdeleted) and Task 6 (3.2 MBBridge.webmno longer downloaded on first load).
Live Verification (all tasks confirmed)
Checked viacurl -s https://odontox.io/ on 2026-05-14:
| Check | Expected | Result |
|---|---|---|
poppins-500.woff2 preload in HTML | 1 | 2 (also 500 + 700 weights) |
poppins-700.woff2 preload in HTML | 1 | 2 |
@font-face declarations inlined | ≥ 4 | 4 |
gtm-loader script tag | 1 | 1 |
gtag/js?id=AW in HTML | 0 | 0 (deferred out of HTML) |
ruby.png response | 200 text/html (SPA fallback) | 200 text/html (asset gone) |
ruby-icon.avif Cache-Control | immutable | public, max-age=31536000, immutable |
gtm-loader.js Cache-Control | no-cache | no-cache, must-revalidate |
Bridge.webm Cache-Control | immutable | public, max-age=31536000, immutable |
| HTML document size | < baseline 18 MB | 11,954 bytes (HTML only; assets lazy-loaded) |
Tasks Shipped
| # | Task | Commit |
|---|---|---|
| 1 | Preload Poppins 500/700, preconnect to GTM, dns-prefetch Google/DoubleClick | b302398ad |
| 2 | Inline critical.css into <head> (+ reorder before blocking scripts) | 582fdcd7b, 7efedd57f |
| 3 | Defer GTM via 2s timeout + interaction-trigger loader | 3fc4210d0 |
| 4 | Defer Sentry via dynamic import + boot-error queue (+ wrap non-Error replay) | 9bff34867, de52dadfc |
| 5 | Delete dead 11 MB ruby.png, swap 11 icon usages to 4 KB AVIF/WebP | c911d099f |
| 6 | Lazy-mount Bridge.webm (3.2 MB) via IntersectionObserver | 23a3890d1 |
| 7 | (No-op — GlobalLoading already conditionally renders the <video>) | — |
| 8 | Add Cache-Control for AVIF/WEBM/MP4, SWR on PNG, no-cache on unhashed scripts | 9e0bd2a39 |
Key Wins
- 11 MB dead asset eliminated —
ruby.pngwas never referenced in code but shipped with every Cloudflare Pages deploy, adding 11 MB of transfer weight per visitor. Deleted in Task 5. - ~1.8 MB icon waste eliminated per icon usage — 11 usages of full-resolution
ruby.webpfor icon-sized display (12–20 px) swapped to 2–3 KBruby-icon.avif/ruby-icon.webp. - 3.2 MB below-fold video deferred —
Bridge.webmnow only downloads when the section enters the viewport (300 px threshold), saving the full 3.2 MB for the majority of visitors. - GTM (~136 KB) deferred out of the critical render path (2s delay + interaction trigger).
- Sentry SDK (~50 KB gz with replay) split into a dynamic chunk that loads only after the
loadevent. - Critical CSS inlined so the first paint does not wait for the full Tailwind stylesheet to download.
- Immutable Cache-Control on all hashed assets (AVIF, WEBM, MP4) — repeat visitors pay zero transfer cost for these files.
Remaining Gaps / Follow-ups
Lighthouse still flags these as high-impact opportunities on mobile:-
Unused JavaScript — est. savings 1,418 KiB / ~7,200 ms (
BmLNBmX3.js513 KiB,CoNkGPIr.js381 KiB). The main React + vendor bundle ships code for all routes at once. Route-level code-splitting (React.lazy+Suspense) on the SPA router would be the highest-ROI remaining change. -
Oversized
ruby.webphero image — est. savings 1,883 KiB / ~1,350 ms. The 128×128 px hero inRubySection.tsxloads the full 1.9 MBruby.webp. A purpose-built hero AVIF at 256×256 px (128 px × 2× retina) would weigh ~15–40 KB and remove ~1.85 MB from the landing-page payload. -
Oversized
logo.webp— est. savings 278 KiB. The logo loaded fromassets.odontox.iois 278 KB transferred at a small display size; a scaled AVIF/WebP would help. -
Render-blocking Tailwind CSS (
BYCIOhQ7.css, 56 KB) — Lighthouse reports 300 ms wastedMs. Critical CSS is already inlined for first paint; the remaining 56 KB stylesheet blocks rendering of below-the-fold content. Splitting into route-scoped CSS chunks (Vite CSS code-splitting) would eliminate this. - Main thread script evaluation — 4,781 ms on simulated Moto G Power. Directly addressed by item 1 (code-splitting reduces parse + evaluate time).
-
DRY refactor (cosmetic) —
ruby.webpicon usages inRubySection.tsxline 175 anddicom-utils.tsline 266 still reference the full-res WebP. The DICOM PDF template cannot use<picture>, butRubySection’sRubyHeroIconfunction should eventually use a scaled AVIF hero.

