Eliminating Flash of Unstyled Content and Calendar Render Delays in a Sheets-Backed Booking Widget
We identified and resolved two distinct performance issues in the SailJada booking system: a "JADABOOK NOW" flash of unstyled content (FOUC) on hard refresh, and a ~5-second delay before the custom calendar widget populates with availability data. Both stemmed from CSS load timing and API cold-start latency in our Google Apps Script backend.
The Problem: Two User-Facing Issues
During testing of the staging deployment at staging.sailjada.com, users encountered:
- Issue 1 (FOUC): On hard refresh (Cmd+Shift+R), the sticky booking bar displays unstyled text "JADABOOK NOW" for 1–2 seconds before the actual styled page renders.
- Issue 2 (Empty Calendar): When clicking the booking button, a custom calendar widget opens with an empty grid. Users wait ~5 seconds for colored availability dots to appear.
Both issues degraded perceived performance and user confidence in the booking flow.
Root Cause Analysis
Issue 1: FOUC in Sticky Booking Bar
The staging index.html (155 KB, deployed to S3 bucket sailjada-staging-content) contained two critical CSS issues:
- The sticky bar and modal overlay styles were defined in a
<style>block at line 3082 inside the document body, not in the<head>. - A JavaScript snippet at line 3548 dynamically injects a "BOOK NOW" pulse animation into the hero title element
id="jada-title", but this code executed before the CSS was parsed. - On hard refresh, the browser rendered the hero section with unstyled inline text before the body CSS rules cascaded down.
The inline <style> block at line 3082 contained critical rendering styles for .sticky-bar, .modal-overlay, and .pulse-animation. Until the browser parsed this block, those classes had no effect.
Issue 2: Calendar Population Delay
The custom calendar widget in the staging HTML is rendered by a function jadaRenderCal() (invoked at line 3488). This function:
- Draws a 7×6 grid of date cells immediately when the modal opens.
- Only after receiving booked dates from a Google Apps Script API endpoint does it overlay colored dots (Open, Has Charter, Past) onto those cells.
- The API call
jadaFetchBookedDates()invokes a GAS web app athttps://script.google.com/macros/d/{DEPLOYMENT_ID}/useless/doGet(details redacted for security). - On initial page load, this GAS endpoint experiences a cold start (~3–5 seconds) before returning the JSON payload of booked dates.
Users saw an empty calendar because the grid rendered in milliseconds, but the availability data took seconds to arrive from GAS.
Technical Implementation: Fixes Applied
Fix 1: Move Critical CSS to <head>
We identified the sticky bar and pulse animation styles and relocated them from the body <style> block (line 3082) into the document <head>.
Rationale: CSS in the <head> is parsed before the body renders, eliminating FOUC. This ensures the "JADA" title and "BOOK NOW" pulse are properly styled before any DOM content is painted.
Commands used to identify and verify:
grep -n "sticky-bar\|pulse-animation" /tmp/staging-index.html
grep -n "<style>" /tmp/staging-index.html | head -5
After moving the CSS, we redeployed the updated index.html to the S3 staging bucket and invalidated the CloudFront distribution cache:
aws s3 cp ./index.html s3://sailjada-staging-content/index.html
aws cloudfront create-invalidation --distribution-id STAGING_DIST_ID --paths "/*"
Fix 2: Prefetch Booked Dates and Render Calendar Optimistically
We modified the booking modal logic to separate concerns:
- On page load: A prefetch call to
jadaFetchBookedDates()initiates the GAS API request in the background, hidden from the user. - When user clicks "Book Now": The calendar modal opens and immediately renders the grid with dummy/placeholder styling (e.g., all cells gray).
- When availability data arrives: The grid updates in-place with colored dots (Open, Charter, Past) without re-rendering.
The key change was decoupling the calendar render from the API callback. In the original code, jadaRenderCal() only fired inside the jadaFetchBookedDates callback, forcing users to wait for the API round-trip.
We refactored the modal open handler to call jadaRenderCal(emptyData) immediately, then update it asynchronously:
// Original: Calendar only renders after API returns
jadaFetchBookedDates(() => {
jadaRenderCal(); // 5 second delay
});
// Fixed: Calendar renders immediately, data updates asynchronously
function openBookingModal() {
modal.style.display = 'block';
jadaRenderCal(null); // Render grid immediately with placeholder state
// Prefetch or fetch booked dates in background
if (!bookedDatesCache) {
jadaFetchBookedDates((data) => {
jadaUpdateCalendarDots(data); // Update dots without full re-render
});
} else {
jadaUpdateCalendarDots(bookedDatesCache); // Use cached data if available
}
}
Additionally, we added a prefetch call in the main page load initialization (around line 2800) to warm up the GAS backend before the user opens the modal.
Infrastructure and Deployment
The staging environment uses:
- S3 Bucket:
sailjada-staging-content(hostsindex.htmland static assets) - CloudFront Distribution: Staging CDN (invalidation ID: managed via
aws cloudfrontCLI) - GAS Backend: Google Apps Script web app deployed as an executable endpoint (used by both staging and production)
- Response Headers: Verified
Content-Encoding: gzipandCache-Controlheaders viacurl -I
After deploying the fixes, we invalidated the CloudFront cache to ensure browsers received the updated index.html` with CSS in the <head> and the refactored modal logic.