diff --git a/_TODO.md b/_TODO.md index d801f1d9..d90646fd 100644 --- a/_TODO.md +++ b/_TODO.md @@ -43,6 +43,65 @@ https://aws.plainenglish.io/how-to-build-a-chatbot-using-aws-lex-and-lambda-in-2 ## Performance Issues +### FOUC / Setting Theme Styling + +Yes, the concern is legitimate. A theme flash is real, especially when the default HTML ships as light and the user actually wants dark or a custom theme. But the current solution is too expensive: it avoids a theme flash by forcing a blank-screen flash instead, which is worse for both users and Lighthouse. + +The important distinction is this: + +Preventing the wrong theme from painting is a good goal. + +Hiding the entire body until DOMContentLoaded is an overcorrection. + +In your current setup, BaseLayout.astro:140 hides the page, and ThemeInit.astro:105 waits until DOMContentLoaded on first load before removing that hidden state. That means the browser is ready to paint, but your code tells it not to. + +The better pattern is to set the theme before first paint, not to suppress paint. Usually that means: + +- Put a very small inline script in
. +- That script runs immediately, not on DOMContentLoaded. +- It only does the minimum: read localStorage if available, fall back to matchMedia('(prefers-color-scheme: dark)'), and set document.documentElement.dataset.theme. +- Do not touch body at all. +- Do not wait for the DOM to finish parsing. + +That works because document.documentElement already exists while the head is parsing. You do not need the body to exist in order to set the theme token that your CSS reads. + +There are also better structural ways to reduce or eliminate mismatch: + +For explicit user theme choices, store the selected theme in a cookie, not only localStorage. Then the server can render the correct data-theme on the initial HTML. This is the strongest fix. + +For plain light/dark system preference, use CSS prefers-color-scheme where possible. That lets the browser choose the right theme without JavaScript. + +If you need multiple custom themes, use a hybrid: + +- server reads a cookie for explicit chosen theme +- client falls back to a tiny head script for first-time visitors using system preference +Add so browser UI elements align better during startup. + +The practical conclusion is: your concern is valid, but hiding the page is not the right tradeoff. The usual fix is a two-phase approach: + +- Pre-paint phase: tiny inline head script sets the html theme immediately. +- Post-load phase: optional richer theme logic updates meta color, event listeners, analytics, stores, and transition behavior. + +That gives you the theme correctness you want without sacrificing first paint. + +If you want, I can sketch the safest refactor path next without changing code yet, or I can go implement the minimal pre-paint split directly. + +Implemented the pre-paint theme fix. + +The change is in three places. In src/components/Head/index.astro, the theme init script now runs after the head metadata is emitted but still before the body is parsed. In src/components/Head/ThemeInit.astro, the script now resolves the theme and writes data-theme to `` immediately, without waiting for DOMContentLoaded and without touching body visibility. In src/layouts/BaseLayout.astro, the invisible class was removed from ``. + +This keeps the legitimate part of the old design, setting the correct theme before first paint, but removes the blank-screen gate that was delaying FCP and LCP. The script still populates window.metaColors, still updates the theme-color meta tag on initial load, and still handles Astro view transitions. + +## Secondary Contributors + +- The page is also heavier than it needs to be on first load: Lighthouse shows 85 requests and about 676 KiB transferred. The biggest buckets are 50 script requests / 194 KiB, 25 image requests / 287 KiB, and 3 font requests / 119 KiB. + +- The homepage in index.astro pulls in a lot of sections at once. The report shows several carousel cover images loading during initial navigation, and those covers come from index.astro:137. + +- The Backstage section also ships a large homepage image from index.astro:39, and that request is the single largest network item in the report at about 120 KiB. + +- Fonts are not the main regression because fonts.css:40 uses font-display: swap, but they still add noticeable startup weight. + ### Home page size - Audit the homepage hydration/chunk fan-out after prerendering. The 22 JS chunks suggest too much client code is shipping for a marketing landing page. diff --git a/src/components/Head/ThemeInit.astro b/src/components/Head/ThemeInit.astro index 6bb31878..a8dd7668 100644 --- a/src/components/Head/ThemeInit.astro +++ b/src/components/Head/ThemeInit.astro @@ -2,15 +2,13 @@ /** * Theme Initialization - Inline Script * - * Set data-theme attribute on and the element - * from localStorage, prefers-color-scheme as a fallback, or sets a default. + * Set data-theme on as early as possible so the first paint uses the + * correct theme, then sync supporting metadata for the active theme. * - * CRITICAL: This script MUST be inlined (is:inline) and run as early as possible - * to prevent Flash of Unstyled Content (FOUC). + * CRITICAL: This script MUST be inlined (is:inline) and run before the body is + * parsed to avoid flashing the default light theme for dark or custom themes. * * !!! DO NOT EXPAND THE SCOPE OF THIS SCRIPT OR MOVE OUT OF THIS FILE !!! - * - * It runs synchronously and blocks rendering. */ import { themeData } from './server/themeData' @@ -41,72 +39,37 @@ const { defaultThemeIdJson, darkThemeIdJson, metaColorsJson } = themeData() targetDocument.documentElement.dataset['theme'] = themeId } + const applyThemeMetaColor = (targetDocument, themeId) => { + const metaElement = targetDocument.querySelector('meta[name="theme-color"]') + if (metaElement && window.metaColors) { + metaElement.setAttribute('content', window.metaColors[themeId] || '') + } + } + const runThemeInit = () => { - /** - * This code is only for initial page load from the site. DOMContentLoaded does - * not fire when using the Astro View Transitions API for page navigation, so - * the themes store defines a side effect to set up an event listener for the - * "astro:before-swap" event. - */ try { const defaultThemeId = JSON.parse(defaultThemeIdJson) const darkThemeId = JSON.parse(darkThemeIdJson) const themeMetaColors = JSON.parse(metaColorsJson) - /** 1. Read theme preference from localStorage (set by user's previous selection) */ const stored = localStorage.getItem('theme') const resolvedThemeId = getResolvedThemeId({ defaultThemeId, darkThemeId }) + const activeThemeId = stored && stored !== defaultThemeId ? stored : resolvedThemeId + + applyThemeToDocument(document, activeThemeId) - if (stored && stored !== defaultThemeId) { - /** User explicitly chose a theme - apply it immediately */ - /** element */ - applyThemeToDocument(document, stored) - } else if (!stored) { - /** No stored preference - use system preference */ - /** element */ - applyThemeToDocument(document, resolvedThemeId) - } - - /** 2. Turn visible. It's set to hidden in BaseLayout.astro to avoid FOUC. Do - * here to make sure it gets turned on, in case the element selector throws. - */ - document.body.classList.remove('invisible') - - /** 3. Add theme entries from src/content/themes.json to the window.metaColors object */ window.metaColors = window.metaColors || {} Object.assign(window.metaColors, themeMetaColors) - /** 4. Update meta theme-color used for PWAs */ - const metaElement = document.querySelector('meta[name="theme-color"]') - if (stored && stored !== defaultThemeId && metaElement && window.metaColors) { - /** User explicitly chose a theme - apply it immediately */ - metaElement.setAttribute('content', window.metaColors[stored] || '') - } else if (!stored && metaElement && window.metaColors) { - /** No stored preference - use system preference */ - metaElement.setAttribute('content', window.metaColors[resolvedThemeId] || '') - } - - /** - * 5. If stored === defaultThemeId, BaseLayout.astro already set it - * on data-theme attribute and Meta.astro already set it on - * , so nothing to do - */ - - /** 6. Success! */ + applyThemeMetaColor(document, activeThemeId) + console.log('🎨 Theme init on initial load executed') } catch (error) { - /** localStorage access can fail (privacy mode, etc.) */ - /** Fall back to BaseLayout's data-theme="light", make sure the page is visible */ - document.body.classList.remove('invisible') console.error('❌ Theme init on initial load failed with errors:', error) } } - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', runThemeInit, { once: true }) - } else { - runThemeInit() - } + runThemeInit() /** * Add the fast theme setting logic for Astro View Transition API navigation events. @@ -117,18 +80,17 @@ const { defaultThemeIdJson, darkThemeIdJson, metaColorsJson } = themeData() try { const defaultThemeId = JSON.parse(defaultThemeIdJson) const darkThemeId = JSON.parse(darkThemeIdJson) - /** 1. Read theme preference from localStorage (set by user's previous selection) */ + const themeMetaColors = JSON.parse(metaColorsJson) const resolvedThemeId = getResolvedThemeId({ defaultThemeId, darkThemeId }) - /** 2. element */ + + window.metaColors = window.metaColors || {} + Object.assign(window.metaColors, themeMetaColors) + applyThemeToDocument(event.newDocument, resolvedThemeId) - /** 3. Turn visible. It's set to hidden in BaseLayout.astro to avoid FOUC */ - event.newDocument.body.classList.remove('invisible') - /** 4. Success! */ + applyThemeMetaColor(event.newDocument, resolvedThemeId) + console.log('🎨 Theme init on "astro:before-swap" executed') } catch (error) { - /** localStorage access can fail (privacy mode, etc.) */ - /** Fall back to BaseLayout's data-theme="light", make sure the page is visible */ - event.newDocument.body.classList.remove('invisible') console.error('❌ Theme init on "astro:before-swap" failed with errors:', error) } }) diff --git a/src/components/Head/index.astro b/src/components/Head/index.astro index 69bbb197..326b3abd 100644 --- a/src/components/Head/index.astro +++ b/src/components/Head/index.astro @@ -43,8 +43,6 @@ const { {/* hoisting issues related to and elements */}