Eliminating Flash of Unstyled Content and Calendar Render Delays in a Custom Booking Widget
During a recent development session, we identified and resolved two critical UX issues affecting the Queen of San Diego booking experience: a visible "JADABOOK NOW" flash on hard refresh (FOUC) and a ~5-second delay before the calendar populated with availability data. This post details the root causes, architectural decisions, and deployment strategy used to fix both issues.
The Problems
Users reported two distinct issues when accessing the booking page:
- Issue 1 (FOUC): Hard refresh (Command-Shift-R) displayed unstyled hero text reading "JADABOOK NOW" for ~500ms before the page rendered correctly. This occurred because critical CSS was loaded late in the document.
- Issue 2 (Calendar Delay): The custom booking calendar opened empty, requiring ~5 seconds for dates to appear. This happened because the calendar rendering logic waited for an external API call to complete before displaying any UI.
Root Cause Analysis
FOUC: CSS Loaded in <body> Instead of <head>
Inspection of the staging index.html file (deployed to S3 bucket qos-staging-web) revealed the sticky bar modal styles were embedded in a <style> block at line 3082, positioned deep within the <body> element rather than in the <head>. The hero pulse animation styles that control the "JADA" / "BOOK NOW" overlay were similarly positioned late in the document.
On hard refresh, the browser's critical rendering path downloads HTML and begins layout before CSS parsing completes. When styles exist in the body, unstyled DOM elements render briefly before styles apply. The specific cascade was:
- Browser downloads index.html
- Renders hero section with unstyled text before reaching line 3082
- Parses pulse animation CSS
- Repaints with correct styling
Calendar Delay: Render Blocked on API Response
The custom calendar widget's render function jadaRenderCal() (line 3488 in staging index.html) was only invoked as a callback inside jadaFetchBookedDates. This function makes a Google Apps Script API request to fetch booked dates for the booking period. The flow was:
- User clicks calendar input
jadaFetchBookedDatesinitiates XHR to GAS endpoint- GAS cold start incurs ~5-second latency
- Only after response,
jadaRenderCal()executes - Calendar grid appears with availability dots
Users perceived this as a broken or missing calendar, when in fact the entire UI was blocked waiting for data.
Technical Solutions Implemented
Fix 1: Move Critical CSS to <head>
We extracted sticky bar and pulse animation CSS from line 3082 (body position) and inserted it into the document <head> before any render-blocking resources. Specifically:
- Identified all
<style>blocks in the body (3082-3150 range) - Extracted selectors:
.sticky-bar,.modal,.pulse-animation,.hero-overlay - Moved extracted CSS into a new
<style>block immediately after the last<meta>tag in<head> - Removed duplicate styles from body location
- Verified no CSS specificity conflicts introduced by the move
This ensures the browser has all hero and modal styling before constructing the render tree, eliminating the flash.
Fix 2: Decouple Calendar Rendering from API Fetch
We refactored the calendar initialization to render immediately with an empty or skeleton state, then asynchronously populate availability data:
// Old pattern (blocked):
function jadaFetchBookedDates() {
fetch(gasEndpoint)
.then(response => response.json())
.then(data => jadaRenderCal(data)); // Render only after API
}
// New pattern (non-blocking):
function jadaInitBooking() {
jadaRenderCal(); // Render calendar immediately with empty grid
jadaFetchBookedDates().then(data => jadaPopulateAvailability(data));
}
function jadaPopulateAvailability(bookedDates) {
// Overlay availability dots onto existing calendar
bookedDates.forEach(date => {
const cell = document.querySelector(`[data-date="${date}"]`);
if (cell) cell.classList.add('has-charter');
});
}
The calendar grid now renders immediately with date cells. When jadaFetchBookedDates completes (after ~5 seconds), we selectively add colored indicator dots to booked date cells without re-rendering the entire widget.
Fix 3: Prefetch Booked Dates on Page Load
To further reduce perceived delay, we added a prefetch request during initial page load:
- Added
<link rel="prefetch">tag in<head>pointing to the GAS API endpoint - Triggered
jadaFetchBookedDates()on DOMContentLoaded event (not on user click) - By the time a user clicks the calendar input, booked dates are likely already cached or in-flight
- Cold start latency is absorbed during page load rather than during interaction
Deployment and Invalidation
Changes were deployed to the staging environment for validation before production:
- Modified file:
/tmp/staging-index.html(local dev copy) - S3 deployment:
aws s3 cp staging-index.html s3://qos-staging-web/index.html - CloudFront invalidation: Invalidated distribution (ID:
E*— staging CDN) with path pattern/*to clear all cached versions - Cache settings: index.html has 0-second TTL; assets have 86400-second (1 day) TTL
Verification involved hard refresh tests (Command-Shift-R) on staging to confirm FOUC elimination and calendar responsiveness testing with throttled network conditions to validate the prefetch strategy.
Architecture Patterns and Trade-offs
Progressive Enhancement: The refactored calendar uses progressive enhancement — the grid renders with all dates available for interaction immediately, and availability indicators (dots, colors) are layered in asynchronously. Users can interact with the calendar before data arrives; they simply see enhanced UI after the API completes.
Why Not Preload Booked Dates at Build Time? Booked dates change frequently based on charter availability. Including this data in the static HTML would require rebuilding and redeploying the site multiple times daily. Fet