Skip to content

improvement(landing): optimize core web vitals and accessibility - #4193

Merged
emir-karabeg merged 3 commits into
stagingfrom
improvement/core-web-vitals
Apr 16, 2026
Merged

improvement(landing): optimize core web vitals and accessibility#4193
emir-karabeg merged 3 commits into
stagingfrom
improvement/core-web-vitals

Conversation

@emir-karabeg

Copy link
Copy Markdown
Collaborator

Summary

  • Code-split AuthModal and DemoRequestModal via next/dynamic across 7 landing components to move the auth-client bundle out of the initial JS payload
  • Replace useSession import in navbar with direct SessionContext read to avoid pulling the entire better-auth client into the landing page bundle
  • Add immutable Cache-Control header for content-hashed _next/static assets
  • Defer PostHog session recording until user identification to skip loading the recorder on anonymous visits
  • Fix accessibility issues flagged by Lighthouse: missing aria-label, inert on aria-hidden wrapper, decorative alt on logos, duplicate link labels

Context

PageSpeed Insights (Apr 15, 2026) shows sim.ai failing Core Web Vitals — LCP 3.4s (mobile) / 2.6s (desktop), Performance score 75 (mobile) / 82 (desktop). Root causes: ~675KB unused JS on landing page, 3.0s JS execution time, poor cache lifetimes for hashed static assets, and PostHog recorder loading on anonymous visits. Accessibility score was 88 (desktop) / 92 (mobile) due to missing labels and focusable elements inside aria-hidden containers.

Changes

Code-split auth modals (hero.tsx, navbar.tsx, features.tsx, collaboration.tsx, pricing.tsx, footer-cta.tsx, landing-preview-panel.tsx)

  • Replace static import { AuthModal } / import { DemoRequestModal } with dynamic() (SSR preserved, client-side code-split)
  • Moves auth-client bundle (better-auth + SSO/Stripe/org plugins) into a separate chunk loaded on interaction

Navbar session optimization (navbar.tsx)

  • Replace import { useSession } from '@/lib/auth/auth-client' with useContext(SessionContext) from session-provider
  • useSession in auth-client.ts is a thin wrapper around the same context, but importing it forces bundling the entire createAuthClient() call with all plugins
  • Direct context read is semantically identical, avoids the bundle cost

Cache headers (next.config.ts)

  • Add Cache-Control: public, max-age=31536000, immutable for /_next/static/:path*
  • These assets have content hashes in filenames — URL changes on deploy, safe to cache indefinitely
  • Matches Vercel's default behavior, ensures parity for self-hosted (Docker) deployments

PostHog session recording deferral (posthog-provider.tsx, session-provider.tsx)

  • Add disable_session_recording: true to PostHog init config — prevents recorder JS (~80KB) from loading on anonymous visits
  • Call posthog.startSessionRecording() after posthog.identify() for authenticated users
  • Called without overrides to respect dashboard-level sampling and linked flag config

Accessibility fixes (footer.tsx, navbar.tsx, landing-preview-home.tsx, templates.tsx)

  • Set alt='' on logo images inside links that already have aria-label and sr-only text (image is decorative)
  • Add aria-label='Submit message' to icon-only button in landing preview
  • Add inert attribute alongside aria-hidden='true' on ReactFlow preview wrapper to prevent focusable descendants from tab order
  • Rename duplicate "API" footer link to "API Block" to disambiguate from "API" in Product column

Type of Change

  • Improvement
  • Bug fix

Testing

  • Pre-commit hooks (biome lint/format) pass
  • All modal triggers render in SSR (no ssr: false — buttons visible in initial HTML)
  • Auth state in navbar reads from same SessionContext provided by root layout SessionProvider
  • PostHog disable_session_recording and startSessionRecording() verified against posthog-js 1.364.4 type definitions
  • _next/static cache rule only applies to content-hashed assets — safe for immutable caching
  • No styling changes — all diffs are import swaps, config additions, or HTML attribute additions

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

Code-split AuthModal and DemoRequestModal via next/dynamic across 7 landing
components to move auth-client bundle (~150-250KB) out of the initial JS payload.
Replace useSession import in navbar with direct SessionContext read to avoid
pulling the entire better-auth client into the landing page bundle. Add immutable
cache header for content-hashed _next/static assets. Defer PostHog session
recording until user identification to avoid loading the recorder (~80KB) on
anonymous visits. Fix accessibility issues flagged by Lighthouse: add missing
aria-label on preview submit button, add inert to aria-hidden ReactFlow wrapper,
set decorative alt on logos inside labeled links, disambiguate duplicate footer
API links.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel

vercelBot commented Apr 16, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
docsSkippedSkippedApr 16, 2026 2:34am

Request Review

@cursor

cursorBot commented Apr 16, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Moderate risk due to changes in auth/session wiring and PostHog recording behavior; bundle-splitting and a11y tweaks are otherwise low impact.

Overview
Improves landing-page performance by code-splitting AuthModal (and DemoRequestModal where used) via next/dynamic across multiple landing components, reducing initial JS and deferring auth/demo code until interaction.

Updates the landing navbar to read auth state directly from SessionContext (instead of importing the auth client hook) to avoid pulling additional auth-client code into the landing bundle.

Tightens analytics/a11y behavior: disables PostHog session recording at init and starts recording only after user identification, adds missing aria-label for icon-only submit buttons, applies inert to aria-hidden template previews, makes logo images decorative with alt='', and renames the footer “API” block link to “API Block`.”

Reviewed by Cursor Bugbot for commit 9bc82bf. Configure here.

@emir-karabeg

Copy link
Copy Markdown
CollaboratorAuthor

@cursor review

@greptile-apps

greptile-appsBot commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR reduces initial JS payload on the landing page by code-splitting AuthModal and DemoRequestModal via next/dynamic, replaces useSession (which drags in the full auth-client bundle) with a direct useContext(SessionContext) read in the navbar, defers PostHog session recording until after user identification, and fixes several Lighthouse accessibility findings. The implementation is correct and consistent across all 7 affected components.

  • The PR description states that a Cache-Control: public, max-age=31536000, immutable header was added for /_next/static/:path* in next.config.ts, but next.config.ts is not in the changeset and the file contains no such rule — this optimization is missing for self-hosted (Docker) deployments, though Vercel handles it automatically.

Confidence Score: 5/5

Safe to merge — all code changes are correct; one stated optimization (static asset cache headers) is missing from the diff but doesn't affect correctness.

All four optimizations are structurally sound: dynamic imports use the correct named-export pattern with SSR enabled, the SessionContext swap uses safe fallback values, PostHog deferral is guarded against duplicate recording calls, and accessibility fixes are correct. The only finding is a P2 gap between the PR description and the actual changeset (missing _next/static cache header in next.config.ts), which doesn't break any user-facing behavior on Vercel but leaves self-hosted deployments without the claimed caching benefit.

No files require special attention; the missing change is in next.config.ts which is outside the current changeset.

Important Files Changed

FilenameOverview
apps/sim/app/_shell/providers/session-provider.tsxAdds startSessionRecording() after posthog.identify() with a sessionRecordingStarted() guard to prevent duplicate recordings on refetch; effect deps and guard logic are correct.
apps/sim/app/_shell/providers/posthog-provider.tsxAdds disable_session_recording: true to PostHog init; session_recording config block is preserved and will apply when recording is manually started for authenticated users.
apps/sim/app/(landing)/components/navbar/navbar.tsxReplaces useSession (pulls full auth-client) with useContext(SessionContext) using safe fallbacks; AuthModal code-split via dynamic(); logo alt cleared to empty string since link already carries aria-label.
apps/sim/app/(landing)/components/hero/hero.tsxBoth AuthModal and DemoRequestModal correctly converted to dynamic() with named-export .then(m => m.X) pattern; SSR remains enabled (no ssr: false).
apps/sim/app/(landing)/components/templates/templates.tsxAdds inert alongside aria-hidden='true' on the ReactFlow preview wrapper to remove focusable descendants from tab order — correct accessibility fix.
apps/sim/app/(landing)/components/footer/footer.tsxLogo alt cleared to empty string (decorative inside an aria-label-bearing link); "API" block link renamed to "API Block" to disambiguate from Product column.

Sequence Diagram

sequenceDiagram
participant Browser
participant Next.js SSR
participant SessionProvider
participant PostHogProvider
participant PostHog
Browser->>Next.js SSR: Request landing page
Next.js SSR-->>Browser: HTML (navbar buttons, hero CTAs rendered)
Note over Browser: AuthModal/DemoRequestModal JS NOT in initial bundle
Browser->>PostHogProvider: useEffect → posthog.init(disable_session_recording: true)
Browser->>SessionProvider: useEffect → loadSession()
SessionProvider-->>Browser: session data returned
alt User is authenticated
SessionProvider->>PostHog: posthog.identify(user.id, ...)
SessionProvider->>PostHog: sessionRecordingStarted()? → false
SessionProvider->>PostHog: startSessionRecording()
else Anonymous visitor
SessionProvider->>PostHog: posthog.reset()
Note over PostHog: Recording stays disabled (~80KB saved)
end
Browser->>Browser: User clicks CTA button
Browser->>Next.js: dynamic import → AuthModal chunk loaded
Browser-->>Browser: Modal opens
Loading

Reviews (2): Last reviewed commit: "fix(config): remove redundant _next/stat..." | Re-trigger Greptile

Comment threadapps/sim/app/_shell/providers/session-provider.tsx Outdated
…efetch
The effect fires on every session reload (e.g., subscription upgrade).
Calling startSessionRecording() while already recording fragments the
session in the analytics dashboard. Add sessionRecordingStarted() guard
so recording only starts once per page lifecycle.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment threadapps/sim/next.config.ts Outdated
Next.js already sets Cache-Control: public, max-age=31536000, immutable
on _next/static assets natively and this cannot be overridden. The custom
rule was redundant on Vercel and conflicted with the extension-based rule
on self-hosted deployments due to last-match-wins ordering.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@emir-karabeg

Copy link
Copy Markdown
CollaboratorAuthor

@cursor review

@emir-karabeg

Copy link
Copy Markdown
CollaboratorAuthor

@greptile

@cursorcursorBot 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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 9bc82bf. Configure here.

@emir-karabeg
emir-karabeg merged commit 23ccd4a into stagingApr 16, 2026
14 checks passed
@emir-karabeg
emir-karabeg deleted the improvement/core-web-vitals branch April 16, 2026 02:56
Sign up for freeto 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.

1 participant

@emir-karabeg