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.
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.
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.
ID: booking-calendar-availability.calendar-display.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:
Suspense, skeleton UI, loading.tsx, or async fetch patterns.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