Calendar loads initial view within 2s on 3G and does not block first paint
Why it matters
A blank screen for 4 seconds while the availability API resolves is the leading cause of mobile booking abandonment. Synchronous data fetching on calendar pages blocks first paint, penalizes users on 3G connections (typical in mobile-first markets), and tanks Core Web Vitals LCP scores. ISO 25010 time-behaviour requires that interactive systems provide perceptible feedback within 1–2 seconds. Blocking fetches also mean that a slow or erroring availability API takes down the entire page instead of degrading gracefully to a skeleton.
Severity rationale
Low because blocking load primarily affects mobile users on slow connections rather than corrupting data, but it is the dominant cause of booking page abandonment on 3G.
Remediation
Wrap the data-fetching calendar component in React Suspense with a skeleton fallback so the page shell renders immediately while availability loads in the background.
// src/app/booking/page.tsx
import { Suspense } from 'react'
import { CalendarContent } from '@/components/CalendarContent'
import { CalendarSkeleton } from '@/components/CalendarSkeleton'
export default function BookingPage() {
return (
<Suspense fallback={<CalendarSkeleton />}>
<CalendarContent />
</Suspense>
)
}
Create src/app/booking/loading.tsx as a fallback for the whole route:
export default function Loading() {
return <CalendarSkeleton />
}
The skeleton should have the same grid dimensions as the real calendar to avoid layout shift when data arrives.
Detection
-
ID:
performance-load-time -
Severity:
low -
What to look for: Enumerate all resources loaded on the calendar page: JS bundles, CSS files, API calls. Count render-blocking resources (scripts without
async/defer, CSS without media queries). Check for:- Render-blocking resources — large CSS/JS bundles loaded before calendar renders.
- Waterfall timing — API call completion time before content appears.
- Non-blocking data loading —
Suspense, skeleton UI,loading.tsx, or async fetch patterns. - Slot data loading — verify availability API is non-blocking (async).
Examine files matching
src/app/*booking*/page.tsx,src/app/*booking*/loading.tsx,src/components/*Calendar*,src/components/*Skeleton*.
-
Pass criteria: Calendar page must use at least 1 non-blocking data loading pattern (React Suspense, skeleton fallback,
loading.tsx, or async component). No more than 0 render-blocking scripts may exist in the calendar page's critical path. The availability API call must be async and not block initial paint. Report even on pass: "Loading strategy: [Suspense/skeleton/loading.tsx]. Render-blocking resources: [count]." -
Fail criteria: Calendar takes >2 seconds to show any visual content on simulated 3G, or render is blocked by a synchronous availability API call with no fallback UI.
-
Skip (N/A) when: Calendar is fully server-rendered with data embedded in the initial HTML response, or project has no calendar page to evaluate.
-
Detail on fail: Example:
"On 3G, the page is blank for 4 seconds waiting for the availability API. Calendar renders at 4.2s. First Paint is at 3.8s due to render-blocking JavaScript." -
Cross-reference: Check
data-freshness— fresh API calls on every load must not block first paint; use Suspense. -
Cross-reference: Check
responsive-rendering— mobile devices on slow networks are most affected by blocking resources. -
Cross-reference: Check
live-availability-update— post-booking refetch should also not block the UI. -
Remediation: Defer availability data loading and render a skeleton or placeholder first:
import { Suspense } from 'react' export function CalendarPage() { return ( <div> <h1>Calendar</h1> <Suspense fallback={<CalendarSkeleton />}> <CalendarContent /> </Suspense> </div> ) } async function CalendarContent() { // This data loads in the background; Suspense shows skeleton until it's ready const slots = await fetch('/api/availability') return <Calendar slots={slots} /> } function CalendarSkeleton() { return ( <div className="grid grid-cols-7 gap-1"> {Array.from({ length: 35 }).map((_, i) => ( <div key={i} className="h-12 bg-gray-200 rounded animate-pulse" /> ))} </div> ) }Also ensure your availability API endpoint uses caching headers to reduce request time on repeat loads:
res.setHeader('Cache-Control', 'public, max-age=60') // Cache for 60 seconds
External references
- iso-25010:2011 · time-behaviour — Time Behaviour (Performance Efficiency) — calendar must meet 2s load threshold
Taxons
History
- 2026-04-18·v1.0.0·Initial import from booking-calendar-availability·automated