refactor: optimize theme script to prevent render-blocking (#601)

* refactor: optimize theme script to prevent render-blocking

- Move theme script from `public/` to `src/scripts/`
- Split into minimal inline script (FOUC prevention) + external script (non-blocking)
- Rename `primaryColorScheme` to `initialColorScheme`
- Rename `toggle-theme.js` to `theme.ts`
- Use TypeScript for theme script
- Update docs for theme customization
This commit is contained in:
Sat Naing
2026-01-10 07:26:25 +07:00
committed by GitHub
parent 25d25437cf
commit 5bb8e405bb
6 changed files with 182 additions and 103 deletions
-88
View File
@@ -1,88 +0,0 @@
const primaryColorScheme = ""; // "light" | "dark"
// Get theme data from local storage
const currentTheme = localStorage.getItem("theme");
function getPreferTheme() {
// return theme value in local storage if it is set
if (currentTheme) return currentTheme;
// return primary color scheme if it is set
if (primaryColorScheme) return primaryColorScheme;
// return user device's prefer color scheme
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}
let themeValue = getPreferTheme();
function setPreference() {
localStorage.setItem("theme", themeValue);
reflectPreference();
}
function reflectPreference() {
document.firstElementChild.setAttribute("data-theme", themeValue);
document.querySelector("#theme-btn")?.setAttribute("aria-label", themeValue);
// Get a reference to the body element
const body = document.body;
// Check if the body element exists before using getComputedStyle
if (body) {
// Get the computed styles for the body element
const computedStyles = window.getComputedStyle(body);
// Get the background color property
const bgColor = computedStyles.backgroundColor;
// Set the background color in <meta theme-color ... />
document
.querySelector("meta[name='theme-color']")
?.setAttribute("content", bgColor);
}
}
// set early so no page flashes / CSS is made aware
reflectPreference();
window.onload = () => {
function setThemeFeature() {
// set on load so screen readers can get the latest value on the button
reflectPreference();
// now this script can find and listen for clicks on the control
document.querySelector("#theme-btn")?.addEventListener("click", () => {
themeValue = themeValue === "light" ? "dark" : "light";
setPreference();
});
}
setThemeFeature();
// Runs on view transitions navigation
document.addEventListener("astro:after-swap", setThemeFeature);
};
// Set theme-color value before page transition
// to avoid navigation bar color flickering in Android dark mode
document.addEventListener("astro:before-swap", event => {
const bgColor = document
.querySelector("meta[name='theme-color']")
?.getAttribute("content");
event.newDocument
.querySelector("meta[name='theme-color']")
?.setAttribute("content", bgColor);
});
// sync with system changes
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", ({ matches: isDark }) => {
themeValue = isDark ? "dark" : "light";
setPreference();
});
@@ -1,7 +1,7 @@
---
author: Sat Naing
pubDatetime: 2022-09-25T15:20:35Z
modDatetime: 2026-01-04T11:27:37.761Z
modDatetime: 2026-01-09T15:00:15.170Z
title: Customizing AstroPaper theme color schemes
featured: false
draft: false
@@ -48,30 +48,43 @@ export const SITE = {
To disable `light & dark mode` set `SITE.lightAndDarkMode` to `false`.
## Choose primary color scheme
## Choose initial color scheme
By default, if we disable `SITE.lightAndDarkMode`, we will only get system's prefers-color-scheme.
Thus, to choose primary color scheme instead of prefers-color-scheme, we have to set color scheme in the `primaryColorScheme` variable inside `toggle-theme.js`.
Thus, to choose an initial color scheme instead of prefers-color-scheme, we have to set color scheme in the `initialColorScheme` variable inside `theme.ts`.
```js file="public/toggle-theme.js"
const primaryColorScheme = ""; // "light" | "dark" // [!code hl]
```ts file="src/scripts/theme.ts"
// Initial color scheme
// Can be "light", "dark", or empty string for system's prefers-color-scheme
const initialColorScheme = ""; // "light" | "dark" // [!code hl]
// Get theme data from local storage
const currentTheme = localStorage.getItem("theme");
function getPreferTheme(): string {
// get theme data from local storage (user's explicit choice)
const currentTheme = localStorage.getItem("theme");
if (currentTheme) return currentTheme;
// return initial color scheme if it is set (site default)
if (initialColorScheme) return initialColorScheme;
// return user device's prefer color scheme (system fallback)
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}
// ...
```
The **primaryColorScheme** variable can hold two values\_ `"light"`, `"dark"`. You can leave the empty string (default) if you don't want to specify the primary color scheme.
The **initialColorScheme** variable can hold two values\_ `"light"`, `"dark"`. You can leave the empty string (default) if you don't want to specify an initial color scheme.
- `""` - system's prefers-color-scheme. (default)
- `"light"` - use light mode as primary color scheme.
- `"dark"` - use dark mode as primary color scheme.
- `"light"` - use light mode as initial color scheme.
- `"dark"` - use dark mode as initial color scheme.
<details>
<summary>Why primaryColorScheme' is not inside config.ts?</summary>
To avoid color flickering on page reload, we have to place the toggle-switch JavaScript codes as early as possible when the page loads. It solves the problem of flickering, but as a trade-off, we cannot use ESM imports anymore.
<summary>Why initialColorScheme is not inside config.ts?</summary>
To avoid color flickering on page reload, we have to place the theme initialization JavaScript code as early as possible when the page loads. The theme script is split into two parts: a minimal inline script in the `<head>` that sets the theme immediately, and the full script that loads asynchronously. This approach prevents FOUC (Flash of Unstyled Content) while maintaining optimal performance.
</details>
## Customize color schemes
@@ -68,8 +68,8 @@ In this section, you will find instructions on how to add support for LaTeX in y
---
<!doctype html>
<!-- others... -->
<script is:inline src="/toggle-theme.js"></script>
<!-- Other elements -->
<meta property="og:image" content={socialImageURL} />
<!-- [!code highlight:4] -->
<link
+9
View File
@@ -0,0 +1,9 @@
interface Window {
theme?: {
themeValue: string;
setPreference: () => void;
reflectPreference: () => void;
getTheme: () => string;
setTheme: (val: string) => void;
};
}
+32 -1
View File
@@ -130,10 +130,41 @@ const structuredData = {
<ClientRouter />
<script is:inline src="/toggle-theme.js"></script>
<!-- Minimal inline script to prevent FOUC - sets theme immediately -->
<script is:inline>
(function () {
const initialColorScheme = ""; // "light" | "dark"
const currentTheme = localStorage.getItem("theme");
function getPreferTheme() {
if (currentTheme) return currentTheme;
if (initialColorScheme) return initialColorScheme;
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}
const themeValue = getPreferTheme();
// Set theme immediately to prevent flash
document.firstElementChild?.setAttribute("data-theme", themeValue);
// Export minimal API for external script
window.theme = {
themeValue: themeValue,
getTheme: () => window.theme.themeValue,
setTheme: val => {
window.theme.themeValue = val;
},
};
})();
</script>
</head>
<body>
<slot />
<!-- Load full theme logic -->
<script src="../scripts/theme.ts"></script>
</body>
</html>
+114
View File
@@ -0,0 +1,114 @@
// Constants
const THEME = "theme";
const LIGHT = "light";
const DARK = "dark";
// Initial color scheme
// Can be "light", "dark", or empty string for system's prefers-color-scheme
const initialColorScheme = "";
function getPreferTheme(): string {
// get theme data from local storage (user's explicit choice)
const currentTheme = localStorage.getItem(THEME);
if (currentTheme) return currentTheme;
// return initial color scheme if it is set (site default)
if (initialColorScheme) return initialColorScheme;
// return user device's prefer color scheme (system fallback)
return window.matchMedia("(prefers-color-scheme: dark)").matches
? DARK
: LIGHT;
}
// Use existing theme value from inline script if available, otherwise detect
let themeValue = window.theme?.themeValue ?? getPreferTheme();
function setPreference(): void {
localStorage.setItem(THEME, themeValue);
reflectPreference();
}
function reflectPreference(): void {
document.firstElementChild?.setAttribute("data-theme", themeValue);
document.querySelector("#theme-btn")?.setAttribute("aria-label", themeValue);
// Get a reference to the body element
const body = document.body;
// Check if the body element exists before using getComputedStyle
if (body) {
// Get the computed styles for the body element
const computedStyles = window.getComputedStyle(body);
// Get the background color property
const bgColor = computedStyles.backgroundColor;
// Set the background color in <meta theme-color ... />
document
.querySelector("meta[name='theme-color']")
?.setAttribute("content", bgColor);
}
}
// Update the global theme API
if (window.theme) {
window.theme.setPreference = setPreference;
window.theme.reflectPreference = reflectPreference;
} else {
window.theme = {
themeValue,
setPreference,
reflectPreference,
getTheme: () => themeValue,
setTheme: (val: string) => {
themeValue = val;
},
};
}
// Ensure theme is reflected (in case body wasn't ready when inline script ran)
reflectPreference();
function setThemeFeature(): void {
// set on load so screen readers can get the latest value on the button
reflectPreference();
// now this script can find and listen for clicks on the control
document.querySelector("#theme-btn")?.addEventListener("click", () => {
themeValue = themeValue === LIGHT ? DARK : LIGHT;
window.theme?.setTheme(themeValue);
setPreference();
});
}
// Set up theme features after page load
setThemeFeature();
// Runs on view transitions navigation
document.addEventListener("astro:after-swap", setThemeFeature);
// Set theme-color value before page transition
// to avoid navigation bar color flickering in Android dark mode
document.addEventListener("astro:before-swap", event => {
const astroEvent = event;
const bgColor = document
.querySelector("meta[name='theme-color']")
?.getAttribute("content");
if (bgColor) {
astroEvent.newDocument
.querySelector("meta[name='theme-color']")
?.setAttribute("content", bgColor);
}
});
// sync with system changes
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", ({ matches: isDark }) => {
themeValue = isDark ? DARK : LIGHT;
window.theme?.setTheme(themeValue);
setPreference();
});