Eliminating Calendar FOUC and Lazy-Loading Delays: A Case Study in Frontend Performance Optimization
During a recent development sprint, we identified two distinct user-facing performance issues on the staging booking interface: a flash of unstyled content (FOUC) displaying "JADABOOK NOW" on hard refresh, and a ~5-second delay before the custom calendar widget populated with availability data. Both issues stemmed from CSS and data-loading architecture decisions that needed refinement. This post details the root causes, fixes, and infrastructure changes we deployed.
Problem Statement: Two Symptoms, Two Root Causes
Our QA team reported that:
- Issue #1: On Command+Shift+R (hard refresh), the hero section displayed a broken "JADABOOK NOW" text overlay for ~200ms before rendering correctly
- Issue #2: When users clicked the booking calendar, the widget opened with an empty grid; dates populated ~5 seconds later after an API call completed
These are classic symptoms of render-blocking CSS and lazy-loaded data, but the actual implementation details required careful inspection of our 155KB minified staging index file and infrastructure setup.
Root Cause Analysis: FOUC from Body-Level Styles
The staging /tmp/staging-index.html deployed to our S3 bucket contained a <style> block at line 3082 inside the <body> element, not in the <head>. This style block contained critical CSS for the sticky booking bar and modal overlay:
/* Lines 3082-3150 in staging index.html (BEFORE fix) */
<body>
<!-- Hero section, nav, etc. -->
<style>
.sticky-bar { position: fixed; bottom: 0; ... }
.modal-overlay { opacity: 0; transition: opacity 0.3s; }
.pulse-text { animation: pulse 2s infinite; }
</style>
<!-- Rest of DOM -->
</body>
The browser parses HTML sequentially. When it reaches the hero section (which comes before the <style> block in the DOM), it hasn't yet downloaded or parsed the CSS rules for .pulse-text or related animation classes. On a hard refresh with an empty browser cache, the hero title renders unstyled for a brief moment before the inline styles are parsed.
The "JADABOOK NOW" text itself is injected by a script at line 3548 that adds a pulse overlay to the hero title. Without the animation CSS loaded, the overlay appears as raw, unformatted text.
Root Cause Analysis: Calendar Cold-Start Delay
The custom calendar widget is initialized by a jadaRenderCal() function that expects a pre-populated dates array. However, the function is only invoked inside the callback of a jadaFetchBookedDates() call (line 3488):
// Lines 3480-3495 in staging index.html
function jadaFetchBookedDates() {
fetch('https://script.google.com/macros/s/YOUR_GAS_DEPLOYMENT_ID/usercallback')
.then(response => response.json())
.then(data => {
window.bookedDates = data.dates;
jadaRenderCal(); // Calendar only renders AFTER data arrives
})
.catch(err => console.error('Fetch failed:', err));
}
// Called only on calendar open, not on page load
document.getElementById('calendar-toggle').addEventListener('click', jadaFetchBookedDates);
This creates a critical path dependency: the user must click the calendar, wait for the fetch to complete (cold Google Apps Script instances can take 3–7 seconds), and only then see the rendered calendar. The calendar grid itself is in the DOM, but the date cells remain empty until jadaRenderCal() loops through the fetched dates and populates them.
Technical Solution: CSS Move and Prefetch Strategy
Fix #1: Move Critical CSS to Head
We extracted the 70-line <style> block from line 3082 and inserted it into the <head> section, before any DOM elements are rendered:
/* Modified index.html structure */
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JADA Booking</title>
<!-- Critical inline styles moved from body -->
<style>
.sticky-bar { position: fixed; bottom: 0; width: 100%; z-index: 999; ... }
.modal-overlay { position: fixed; top: 0; left: 0; opacity: 0; transition: opacity 0.3s; }
.pulse-text { animation: pulse 2s infinite; }
@keyframes pulse { 0% { opacity: 0.7; } 50% { opacity: 1; } 100% { opacity: 0.7; } }
</style>
</head>
<body>
<!-- Hero, nav, calendar, etc. -->
</body>
By moving critical CSS above the fold, the browser parses and applies these rules before rendering any hero elements, eliminating the FOUC on hard refresh.
Fix #2: Prefetch Booked Dates on Page Load
We added a prefetch call in a new initialization function that runs on DOMContentLoaded, before the user clicks the calendar:
/* New code inserted before line 3480 */
document.addEventListener('DOMContentLoaded', function() {
// Prefetch booked dates immediately
fetch('https://script.google.com/macros/s/YOUR_GAS_DEPLOYMENT_ID/usercallback')
.then(response => response.json())
.then(data => {
window.bookedDates = data.dates;
console.log('Booked dates prefetched:', window.bookedDates.length);
})
.catch(err => console.warn('Prefetch failed, will retry on click:', err));
});
// Render calendar immediately with placeholder dates
function jadaRenderCalPlaceholder() {
const calGrid = document.getElementById('calendar-grid');
// Render empty/neutral state immediately
for (let i = 1; i <= 31; i++) {
const cell = document.createElement('div');
cell.className = 'cal-cell';
cell.textContent = i;
calGrid.appendChild(cell);
}
}
// On calendar toggle, render immediately, then update with booked data
document.getElementById('calendar-toggle').addEventListener('click', function() {
jadaRenderCalPlaceholder();
// If prefetch succeeded, availability dots are already in DOM
if (window.bookedDates) {
jadaRenderCal(); // Update with real data immediately
} else {
// If prefetch failed or is still pending, fetch again
jadaFetchBookedDates();
}
});
This approach:
- Initiates a background fetch of booked dates as soon as the page