Skip to main content

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.
MetricValue
Performance score49 / 100
FCP2,106 ms
LCP3,853 ms
TBT198 ms
CLS0
TTI2,499 ms
Load time6.9 s
Page size~18 MB (Object payload alone: 13 MB)
Total requests78

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)

MetricValue
Performance score27 / 100
FCP7.0 s
LCP19.3 s
TBT2,156 ms
CLS0
Speed Index13.5 s
TTI19.3 s
Total bytes transferred6,722 KiB (~6.6 MB)
Network requests79

Desktop (preset=desktop, Lighthouse v12)

MetricValue
Performance score69 / 100
FCP1.3 s
LCP2.7 s
TBT190 ms
CLS0
Speed Index3.1 s
Total bytes transferred6,779 KiB (~6.6 MB)
Network requests80

Delta (page-weight and requests — throttling-independent)

MetricBaselinePost-optimisationDelta
Page size~18 MB~6.6 MB-63% (-11.4 MB)
Requests7879~flat (+1, noise)
Desktop LCPn/a2.7 s
Desktop Performancen/a69 / 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 MB ruby.png deleted) and Task 6 (3.2 MB Bridge.webm no longer downloaded on first load).

Live Verification (all tasks confirmed)

Checked via curl -s https://odontox.io/ on 2026-05-14:
CheckExpectedResult
poppins-500.woff2 preload in HTML12 (also 500 + 700 weights)
poppins-700.woff2 preload in HTML12
@font-face declarations inlined≥ 44
gtm-loader script tag11
gtag/js?id=AW in HTML00 (deferred out of HTML)
ruby.png response200 text/html (SPA fallback)200 text/html (asset gone)
ruby-icon.avif Cache-Controlimmutablepublic, max-age=31536000, immutable
gtm-loader.js Cache-Controlno-cacheno-cache, must-revalidate
Bridge.webm Cache-Controlimmutablepublic, max-age=31536000, immutable
HTML document size< baseline 18 MB11,954 bytes (HTML only; assets lazy-loaded)

Tasks Shipped

#TaskCommit
1Preload Poppins 500/700, preconnect to GTM, dns-prefetch Google/DoubleClickb302398ad
2Inline critical.css into <head> (+ reorder before blocking scripts)582fdcd7b, 7efedd57f
3Defer GTM via 2s timeout + interaction-trigger loader3fc4210d0
4Defer Sentry via dynamic import + boot-error queue (+ wrap non-Error replay)9bff34867, de52dadfc
5Delete dead 11 MB ruby.png, swap 11 icon usages to 4 KB AVIF/WebPc911d099f
6Lazy-mount Bridge.webm (3.2 MB) via IntersectionObserver23a3890d1
7(No-op — GlobalLoading already conditionally renders the <video>)
8Add Cache-Control for AVIF/WEBM/MP4, SWR on PNG, no-cache on unhashed scripts9e0bd2a39

Key Wins

  • 11 MB dead asset eliminatedruby.png was 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.webp for icon-sized display (12–20 px) swapped to 2–3 KB ruby-icon.avif/ruby-icon.webp.
  • 3.2 MB below-fold video deferredBridge.webm now 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 load event.
  • 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:
  1. Unused JavaScript — est. savings 1,418 KiB / ~7,200 ms (BmLNBmX3.js 513 KiB, CoNkGPIr.js 381 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.
  2. Oversized ruby.webp hero image — est. savings 1,883 KiB / ~1,350 ms. The 128×128 px hero in RubySection.tsx loads the full 1.9 MB ruby.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.
  3. Oversized logo.webp — est. savings 278 KiB. The logo loaded from assets.odontox.io is 278 KB transferred at a small display size; a scaled AVIF/WebP would help.
  4. 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.
  5. Main thread script evaluation — 4,781 ms on simulated Moto G Power. Directly addressed by item 1 (code-splitting reduces parse + evaluate time).
  6. DRY refactor (cosmetic)ruby.webp icon usages in RubySection.tsx line 175 and dicom-utils.ts line 266 still reference the full-res WebP. The DICOM PDF template cannot use <picture>, but RubySection’s RubyHeroIcon function should eventually use a scaled AVIF hero.

How to Re-Measure

CHROME_PATH="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
npx -y lighthouse@12 https://odontox.io/ \
  --form-factor=mobile \
  --throttling-method=simulate \
  --output=html --output-path=/tmp/lh-odontox-mobile.html \
  --chrome-flags="--headless=new --no-sandbox --disable-dev-shm-usage"
Or run Chrome DevTools → Lighthouse panel → Mobile → Analyse page load.