Skip to content

perf: trim critical-path CSS and warm map/data connections - #10

Merged
nedcut merged 4 commits into
mainfrom
claude/site-performance-optimization-cgvck2
Jun 18, 2026
Merged

perf: trim critical-path CSS and warm map/data connections#10
nedcut merged 4 commits into
mainfrom
claude/site-performance-optimization-cgvck2

Conversation

@nedcut

@nedcut nedcut commented Jun 17, 2026

Copy link
Copy Markdown
Owner

Summary

Performance pass focused on the critical rendering path — the bytes and round-trips between request and first interactive paint — without changing any behavior or visual output. I audited the bundle, measured a baseline production build, applied the changes, and re-measured.

The codebase was already well-optimized (React Compiler on; mapbox-gl, exifreader, and all heavy panels behind dynamic(); Turf imported by subpath; system fonts only), so the wins target what still ships eagerly to every visitor.

Changes

  1. Move the vendored mapbox-gl stylesheet off the critical path. It was @imported in globals.css — render-blocking on every load — even though the map JS is lazy (dynamic(ssr:false)). Moved the import into MapView.tsx (the only consumer), so it ships in the lazy map chunk instead.
  2. Preconnect / dns-prefetch hints for Mapbox (api/events) and the Supabase origin, so TLS handshakes for tiles, styles, and photos start during HTML parse rather than after the map/gallery code executes.
  3. decoding="async" on the Journey hero slideshow images to keep large photo decodes off the main thread.

Quantitative results

Measured from next build (Next 16 / Turbopack) artifacts, before vs. after.

Render-blocking critical CSS (downloaded before first paint)

Raw Gzip
Before 108.5 KB 17.7 KB
After 68.7 KB 12.4 KB
Δ −39.8 KB (−37%) −5.3 KB (−30%)

The 39.8 KB / 5.4 KB-gzip mapbox stylesheet now loads as a separate chunk alongside the map JS. Verified the prerendered index.html <head> references only the app CSS chunk (mapbox CSS occurrences in head: 0).

Network round-trips

5 resource hints now emitted in the initial document, warming the connections to api.mapbox.com, events.mapbox.com, and the Supabase origin before the deferred map/data code runs — saves ~1 RTT (DNS+TCP+TLS) per origin on first paint.

For reference — initial JS (unchanged, already optimal)

~302 KB gzip, dominated by @supabase/supabase-js (~90 KB gz, the data client) and react-dom (~71 KB gz) — both genuinely on the critical path. The 1.7 MB / 467 KB-gzip mapbox-gl engine stays correctly code-split into its own lazy chunk.

Verification

  • npm run build
  • npm run lint ✓ (1 pre-existing, unrelated warning)
  • npm run typecheck
  • npm test ✓ — 192/192 passing

🤖 Generated with Claude Code


Generated by Claude Code

Summary by CodeRabbit

  • Chores
    • Optimized initial load by loading the Mapbox base stylesheet with the map view instead of globally.
    • Reduced Supabase connection latency with added preconnect and dns-prefetch hints for the detected origin.
  • Style
    • Added dedicated Mapbox UI overrides, including customized popups, controls, photo markers, and popup card layouts (with reduced-motion support).
  • Performance
    • Improved slideshow rendering by enabling async image decoding.
    • Faster route and GPX-related computations via lighter, on-demand processing and local distance calculations.

Move the vendored mapbox-gl stylesheet (~40KB raw / 5.4KB gzip) out of the
render-blocking globals.css and into MapView, which is loaded via a lazy
dynamic(ssr:false) import. The stylesheet now ships with the map chunk instead
of every first paint, cutting render-blocking critical CSS from 108.5KB to
68.7KB raw (17.7KB -> 12.4KB gzip, -30%) with no visual change — MapView is the
only consumer of those styles.

Add preconnect/dns-prefetch resource hints for Mapbox (api/events) and the
Supabase origin so the TLS handshakes for tiles, styles and photos start during
HTML parse rather than after the map/gallery code runs.

Add decoding="async" to the Journey hero slideshow images so large photo
decodes stay off the main thread.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bu6yr5RHkXvk7kXYdL24tu
Copilot AI review requested due to automatic review settings June 17, 2026 22:37

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 was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bde36068-5d79-43b5-9e10-af6eb95e83d5

📥 Commits

Reviewing files that changed from the base of the PR and between e4f0619 and b79c920.

📒 Files selected for processing (3)
  • lib/geo.ts
  • lib/hooks/useTripMutations.ts
  • lib/photo-outliers.ts

📝 Walkthrough

Walkthrough

Mapbox CSS and styling overrides are relocated from the global stylesheet to a dedicated map-overrides.css file imported alongside the lazy-loaded MapView component, reducing initial page-load impact. Simultaneously, RootLayout gains preconnect and dns-prefetch hints for the Supabase origin, and slideshow images in JourneyHeroCard receive the decoding="async" attribute. The Turf distance dependency is removed and replaced with an inlined haversine implementation, with a new distanceKm export for shared use. GPX parsing helpers are lazy-loaded within the importGpx function, and distance utilities are consolidated.

Changes

Page Load Performance and Bundle Size Optimization

Layer / File(s) Summary
Mapbox CSS and styling moved to lazy-loaded map chunk
app/globals.css, components/MapView.tsx, components/map-overrides.css
Mapbox's bundled CSS import is relocated from globals.css to MapView.tsx. All Mapbox-specific styling overrides (popups, controls, photo markers, animations, card layouts) are extracted into a new components/map-overrides.css file and imported in MapView.tsx so both vendor and override styles ship with the lazy map chunk. globals.css retains only hero animation styles.
Supabase preconnect and dns-prefetch hints in RootLayout
app/layout.tsx
supabaseOrigin is derived from NEXT_PUBLIC_SUPABASE_URL via URL(...).origin with a try/catch fallback to null. When non-null, preconnect and dns-prefetch link elements for that origin are rendered in <head> to warm the connection.
Async image decoding on slideshow frames
components/JourneyHeroCard.tsx
decoding="async" attribute added to the slideshow <img> element to prevent image decoding from blocking render.
Turf dependency replacement with haversine implementation
lib/geo.ts
Turf's @turf/length dependency is removed. An inline great-circle (haversine) distance calculator with EARTH_RADIUS_METERS constant replaces all distance computations. A new exported distanceKm(a, b) function provides point-to-point distances. coordinateBounds, lineDistanceMeters, and routeFeatureCollection are updated to use the haversine logic instead of Turf.
GPX lazy-loading and distance utility import consolidation
lib/hooks/useTripMutations.ts, lib/photo-outliers.ts
useTripMutations dynamically imports GPX parsing helpers inside importGpx to keep GPX simplify dependencies out of the initial bundle. photo-outliers.ts is updated to import distanceKm from lib/geo instead of lib/journey-leg.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

  • nedcut/lofoten-map#9: Updates JourneyHeroCard.tsx with slideshow image optimizations, overlapping with the decoding="async" attribute addition in this PR.

Poem

🐇 The Mapbox styles now travel light,
Bundled lazy where they need take flight.
Turf is out, haversine shines bright,
Supabase warms up, images decode right.
GPX waits in the shadows, the rabbit hops faster than ever! 🚀

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main performance optimizations: critical CSS reduction (moving Mapbox stylesheet to lazy chunks) and connection warming (preconnect/dns-prefetch for external services).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/site-performance-optimization-cgvck2

Comment @coderabbitai help to get the list of available commands and usage tips.

@vercel

vercel Bot commented Jun 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lofoten-map Ready Ready Preview, Comment Jun 17, 2026 11:00pm

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
components/MapView.tsx (1)

4-6: Organize Mapbox styles into a separate file for better code maintainability.

The suggestion to move .mapboxgl-* related style overrides from app/globals.css to a map-specific stylesheet (e.g., components/mapbox-overrides.css) imported after mapbox-gl/dist/mapbox-gl.css is a good organizational practice. This improves code clarity by keeping map-specific styling separate from global application styles.

However, the current implementation already mitigates cascade concerns: the Mapbox overrides in app/globals.css (lines 33–69) extensively use !important declarations, ensuring they take precedence over vendor styles regardless of load order.

Suggested refactoring
 import mapboxgl from "mapbox-gl";
 // Loaded here rather than in globals.css so the vendored stylesheet ships with
 // this lazily-imported (ssr:false) map chunk, keeping it off the critical path.
 import "mapbox-gl/dist/mapbox-gl.css";
+import "./mapbox-overrides.css";

Then move .mapboxgl-* and related map control/popup styles from app/globals.css into components/mapbox-overrides.css.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/MapView.tsx` around lines 4 - 6, Extract the Mapbox style
overrides (the `.mapboxgl-*` and related map control/popup styles currently in
`app/globals.css` at lines 33–69) into a new separate stylesheet file named
`components/mapbox-overrides.css`. Then import this new stylesheet in
MapView.tsx immediately after the existing `mapbox-gl/dist/mapbox-gl.css` import
statement to maintain the proper cascade order while keeping map-specific styles
organized separately from global application styles.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@components/MapView.tsx`:
- Around line 4-6: Extract the Mapbox style overrides (the `.mapboxgl-*` and
related map control/popup styles currently in `app/globals.css` at lines 33–69)
into a new separate stylesheet file named `components/mapbox-overrides.css`.
Then import this new stylesheet in MapView.tsx immediately after the existing
`mapbox-gl/dist/mapbox-gl.css` import statement to maintain the proper cascade
order while keeping map-specific styles organized separately from global
application styles.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 630c157a-8bb6-4446-9438-66eeccc789b8

📥 Commits

Reviewing files that changed from the base of the PR and between 71a652c and 75f65f8.

📒 Files selected for processing (4)
  • app/globals.css
  • app/layout.tsx
  • components/JourneyHeroCard.tsx
  • components/MapView.tsx

Follow-up to the mapbox-gl.css relocation: extract the Mapbox control/popup and
custom photo-marker overrides out of globals.css into components/map-overrides.css,
imported by MapView after the vendor sheet. Every selector moved targets DOM that
only exists once the map mounts (verified: mapboxgl/lofoten-photo-marker/
lofoten-popup/lofoten-outlier/map-unavailable classes are referenced only by the
map components, all children of the lazy MapView). The hero Ken Burns animation
and .font-serif stay in globals since they render independently of the map.

This trims render-blocking critical CSS further: 68.7KB -> 63.1KB raw
(12.4KB -> 11.1KB gzip). Combined with the earlier change, critical CSS is down
from 108.5KB to 63.1KB raw (-42%) and 17.7KB to 11.1KB gzip (-37%); the moved
styles now ship in the lazy map chunk.

Addresses CodeRabbit's review nitpick on PR #10.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bu6yr5RHkXvk7kXYdL24tu

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@components/map-overrides.css`:
- Around line 111-132: The `.lofoten-photo-highlight-ring` class applies an
infinite animation without respecting user accessibility preferences. Add a
`@media (prefers-reduced-motion: reduce)` media query block after the keyframes
definition that targets the `.lofoten-photo-highlight-ring` class and disables
or removes the animation property to honor users' reduced motion preferences.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2354115d-246b-4f2c-aeaf-13bc2556694c

📥 Commits

Reviewing files that changed from the base of the PR and between 75f65f8 and bef8091.

📒 Files selected for processing (3)
  • app/globals.css
  • components/MapView.tsx
  • components/map-overrides.css
🚧 Files skipped from review as they are similar to previous changes (1)
  • components/MapView.tsx

Comment thread components/map-overrides.css
The pulsing ring that marks the photo open in the editor ran an infinite
animation with no reduced-motion fallback. Add a prefers-reduced-motion guard so
the ring stays visible but holds still for those users — matching the existing
treatment of the hero Ken Burns animation.

Addresses CodeRabbit's review comment on PR #10.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bu6yr5RHkXvk7kXYdL24tu
Inspired by an alternate PR that swapped @turf/length for an inline haversine in
geo.ts. On its own that saved nothing — Turf still reached the initial page graph
through two other paths — so this carries the idea through to actually remove it:

- lib/geo.ts: inline haversine (segmentDistanceMeters/geometryDistanceMeters)
  replaces @turf/length for bounds and route-distance math, plus a distanceKm
  helper. Uses Turf's mean Earth radius so displayed distances are unchanged.
- lib/photo-outliers.ts: pull distanceKm from geo instead of journey-leg. It only
  needed that one function, but importing it dragged journey-leg's eight @turf/*
  packages into the initial bundle (via AdminDataPanel in the always-mounted
  sidebar). JourneyMiniMap still uses journey-leg in its own lazy chunk.
- lib/hooks/useTripMutations.ts: load @/lib/gpx (and its @turf/simplify dep)
  lazily inside the importGpx callback — a rare, already-async admin action.

Net: Turf is no longer present in the initial JS at all (verified by scanning the
chunks the prerendered document loads). Initial JS drops from ~1032KB to 1014KB
raw / ~302KB to 295.6KB gzip. Turf stays installed for the dynamic journey/upload
chunks that genuinely need it; behavior is unchanged (192 tests pass).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bu6yr5RHkXvk7kXYdL24tu
@nedcut
nedcut merged commit 70b8518 into main Jun 18, 2026
4 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Jul 11, 2026
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.

3 participants