mirror of
https://github.com/ChenQihan666/MyBlog-Next.git
synced 2026-08-14 07:33:07 +08:00
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:
@@ -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
|
||||
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
interface Window {
|
||||
theme?: {
|
||||
themeValue: string;
|
||||
setPreference: () => void;
|
||||
reflectPreference: () => void;
|
||||
getTheme: () => string;
|
||||
setTheme: (val: string) => void;
|
||||
};
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
Reference in New Issue
Block a user