Skip to content

Commit 014f817

Browse files
author
CMSZ
committed
feat: 优化搜索和主题切换的初始加载性能
- 将搜索栏和主题切换按钮移至 Astro 组件以实现即时渲染 - 预加载关键图标以消除 CDN 请求延迟 - 将 Pagefind 脚本加载逻辑移至主布局,减少重复代码 - 通过 data-theme-mode 属性同步主题状态,避免样式闪烁 - [#725](saicaca/fuwari#725)
1 parent d844039 commit 014f817

File tree

4 files changed

+176
-112
lines changed

4 files changed

+176
-112
lines changed

src/components/LightDarkSwitch.svelte

Lines changed: 53 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@
22
import { AUTO_MODE, DARK_MODE, LIGHT_MODE } from "@constants/constants.ts";
33
import I18nKey from "@i18n/i18nKey";
44
import { i18n } from "@i18n/translation";
5-
import Icon from "@iconify/svelte";
5+
import Icon, { addIcon } from "@iconify/svelte";
6+
import icons from '@iconify-json/material-symbols/icons.json';
7+
// Preload icons to avoid CDN requests
8+
['wb-sunny-outline-rounded', 'dark-mode-outline-rounded', 'radio-button-partial-outline'].forEach(name => {
9+
if (icons.icons[name]) {
10+
addIcon(`material-symbols:${name}`, { body: icons.icons[name].body, width: icons.width, height: icons.height });
11+
}
12+
});
613
import {
714
applyThemeToDocument,
815
getStoredTheme,
@@ -11,11 +18,30 @@ import {
1118
import { onMount } from "svelte";
1219
import type { LIGHT_DARK_MODE } from "@/types/config.ts";
1320
21+
let {} = $props();
22+
1423
const seq: LIGHT_DARK_MODE[] = [LIGHT_MODE, DARK_MODE, AUTO_MODE];
15-
let mode: LIGHT_DARK_MODE = $state(AUTO_MODE);
24+
let mode: LIGHT_DARK_MODE = $state(
25+
typeof document !== 'undefined'
26+
? (document.documentElement.getAttribute('data-theme-mode') as LIGHT_DARK_MODE || AUTO_MODE)
27+
: AUTO_MODE
28+
);
1629
1730
onMount(() => {
1831
mode = getStoredTheme();
32+
document.documentElement.setAttribute('data-theme-mode', mode);
33+
34+
// Wire up the button that's rendered in Navbar.astro
35+
const button = document.getElementById('scheme-switch');
36+
const wrapper = document.getElementById('theme-switch-wrapper');
37+
if (button) {
38+
button.onclick = toggleScheme;
39+
button.onmouseenter = showPanel;
40+
}
41+
if (wrapper) {
42+
wrapper.onmouseleave = hidePanel;
43+
}
44+
1945
const darkModePreference = window.matchMedia("(prefers-color-scheme: dark)");
2046
const changeThemeWhenSchemeChanged: Parameters<
2147
typeof darkModePreference.addEventListener<"change">
@@ -34,6 +60,7 @@ onMount(() => {
3460
function switchScheme(newMode: LIGHT_DARK_MODE) {
3561
mode = newMode;
3662
setTheme(newMode);
63+
document.documentElement.setAttribute('data-theme-mode', newMode);
3764
}
3865
3966
function toggleScheme() {
@@ -57,43 +84,29 @@ function hidePanel() {
5784
}
5885
</script>
5986

60-
<!-- z-50 make the panel higher than other float panels -->
61-
<div class="relative z-50" role="menu" tabindex="-1" onmouseleave={hidePanel}>
62-
<button aria-label="Light/Dark Mode" role="menuitem" class="relative btn-plain scale-animation rounded-lg h-11 w-11 active:scale-90" id="scheme-switch" onclick={toggleScheme} onmouseenter={showPanel}>
63-
<div class="absolute" class:opacity-0={mode !== LIGHT_MODE}>
64-
<Icon icon="material-symbols:wb-sunny-outline-rounded" class="text-[1.25rem]"></Icon>
65-
</div>
66-
<div class="absolute" class:opacity-0={mode !== DARK_MODE}>
67-
<Icon icon="material-symbols:dark-mode-outline-rounded" class="text-[1.25rem]"></Icon>
68-
</div>
69-
<div class="absolute" class:opacity-0={mode !== AUTO_MODE}>
70-
<Icon icon="material-symbols:radio-button-partial-outline" class="text-[1.25rem]"></Icon>
71-
</div>
72-
</button>
73-
74-
<div id="light-dark-panel" class="hidden lg:block absolute transition float-panel-closed top-11 -right-2 pt-5" >
75-
<div class="card-base float-panel p-2">
76-
<button class="flex transition whitespace-nowrap items-center !justify-start w-full btn-plain scale-animation rounded-lg h-9 px-3 font-medium active:scale-95 mb-0.5"
77-
class:current-theme-btn={mode === LIGHT_MODE}
78-
onclick={() => switchScheme(LIGHT_MODE)}
79-
>
80-
<Icon icon="material-symbols:wb-sunny-outline-rounded" class="text-[1.25rem] mr-3"></Icon>
81-
{i18n(I18nKey.lightMode)}
82-
</button>
83-
<button class="flex transition whitespace-nowrap items-center !justify-start w-full btn-plain scale-animation rounded-lg h-9 px-3 font-medium active:scale-95 mb-0.5"
84-
class:current-theme-btn={mode === DARK_MODE}
85-
onclick={() => switchScheme(DARK_MODE)}
86-
>
87-
<Icon icon="material-symbols:dark-mode-outline-rounded" class="text-[1.25rem] mr-3"></Icon>
88-
{i18n(I18nKey.darkMode)}
89-
</button>
90-
<button class="flex transition whitespace-nowrap items-center !justify-start w-full btn-plain scale-animation rounded-lg h-9 px-3 font-medium active:scale-95"
91-
class:current-theme-btn={mode === AUTO_MODE}
92-
onclick={() => switchScheme(AUTO_MODE)}
93-
>
94-
<Icon icon="material-symbols:radio-button-partial-outline" class="text-[1.25rem] mr-3"></Icon>
95-
{i18n(I18nKey.systemMode)}
96-
</button>
97-
</div>
87+
<!-- Button is rendered in Navbar.astro for instant display -->
88+
<div id="light-dark-panel" class="hidden lg:block absolute transition float-panel-closed top-11 -right-2 pt-5" role="menu" tabindex="-1">
89+
<div class="card-base float-panel p-2">
90+
<button class="flex transition whitespace-nowrap items-center !justify-start w-full btn-plain scale-animation rounded-lg h-9 px-3 font-medium active:scale-95 mb-0.5"
91+
class:current-theme-btn={mode === LIGHT_MODE}
92+
onclick={() => switchScheme(LIGHT_MODE)}
93+
>
94+
<Icon icon="material-symbols:wb-sunny-outline-rounded" class="text-[1.25rem] mr-3"></Icon>
95+
{i18n(I18nKey.lightMode)}
96+
</button>
97+
<button class="flex transition whitespace-nowrap items-center !justify-start w-full btn-plain scale-animation rounded-lg h-9 px-3 font-medium active:scale-95 mb-0.5"
98+
class:current-theme-btn={mode === DARK_MODE}
99+
onclick={() => switchScheme(DARK_MODE)}
100+
>
101+
<Icon icon="material-symbols:dark-mode-outline-rounded" class="text-[1.25rem] mr-3"></Icon>
102+
{i18n(I18nKey.darkMode)}
103+
</button>
104+
<button class="flex transition whitespace-nowrap items-center !justify-start w-full btn-plain scale-animation rounded-lg h-9 px-3 font-medium active:scale-95"
105+
class:current-theme-btn={mode === AUTO_MODE}
106+
onclick={() => switchScheme(AUTO_MODE)}
107+
>
108+
<Icon icon="material-symbols:radio-button-partial-outline" class="text-[1.25rem] mr-3"></Icon>
109+
{i18n(I18nKey.systemMode)}
110+
</button>
98111
</div>
99112
</div>

src/components/Navbar.astro

Lines changed: 45 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ let links: NavBarLink[] = navBarConfig.links.map(
1919
return item;
2020
},
2121
);
22+
23+
const themeIcons = [
24+
{ mode: 'light', icon: 'material-symbols:wb-sunny-outline-rounded' },
25+
{ mode: 'dark', icon: 'material-symbols:dark-mode-outline-rounded' },
26+
{ mode: 'auto', icon: 'material-symbols:radio-button-partial-outline' }
27+
];
2228
---
2329
<div id="navbar" class="z-50 onload-animation">
2430
<div class="absolute h-8 left-0 right-0 -top-8 bg-[var(--card-bg)] transition"></div> <!-- used for onload animation -->
@@ -44,14 +50,41 @@ let links: NavBarLink[] = navBarConfig.links.map(
4450
})}
4551
</div>
4652
<div class="flex">
47-
<!--<SearchPanel client:load>-->
48-
<Search client:only="svelte"></Search>
53+
<!-- Search bar for desktop - instant display with Astro icons -->
54+
<div id="search-bar" class="hidden lg:flex transition-all items-center h-11 mr-2 rounded-lg
55+
bg-black/[0.04] hover:bg-black/[0.06] focus-within:bg-black/[0.06]
56+
dark:bg-white/5 dark:hover:bg-white/10 dark:focus-within:bg-white/10
57+
">
58+
<Icon name="material-symbols:search" class="absolute text-[1.25rem] pointer-events-none ml-3 transition my-auto text-black/30 dark:text-white/30" />
59+
<input id="search-input-desktop" placeholder="Search"
60+
class="transition-all pl-10 text-sm bg-transparent outline-0
61+
h-full w-40 active:w-60 focus:w-60 text-black/50 dark:text-white/50"
62+
/>
63+
</div>
64+
65+
<!-- Search toggle button for mobile - instant display -->
66+
<button aria-label="Search Panel" id="search-switch"
67+
class="btn-plain scale-animation lg:!hidden rounded-lg w-11 h-11 active:scale-90">
68+
<Icon name="material-symbols:search" class="text-[1.25rem]" />
69+
</button>
70+
71+
<Search client:idle></Search>
4972
{!siteConfig.themeColor.fixed && (
5073
<button aria-label="Display Settings" class="btn-plain scale-animation rounded-lg h-11 w-11 active:scale-90" id="display-settings-switch">
5174
<Icon name="material-symbols:palette-outline" class="text-[1.25rem]"></Icon>
5275
</button>
5376
)}
54-
<LightDarkSwitch client:only="svelte"></LightDarkSwitch>
77+
<!-- z-50 make the panel higher than other float panels -->
78+
<div class="relative z-50" id="theme-switch-wrapper">
79+
<button aria-label="Light/Dark Mode" class="relative btn-plain scale-animation rounded-lg h-11 w-11 active:scale-90" id="scheme-switch">
80+
{themeIcons.map(({ mode, icon }) => (
81+
<div class={`absolute theme-icon-${mode}`}>
82+
<Icon name={icon} class="text-[1.25rem]" />
83+
</div>
84+
))}
85+
</button>
86+
<LightDarkSwitch client:idle></LightDarkSwitch>
87+
</div>
5588
<button aria-label="Menu" name="Nav Menu" class="btn-plain scale-animation rounded-lg w-11 h-11 active:scale-90 md:!hidden" id="nav-menu-switch">
5689
<Icon name="material-symbols:menu-rounded" class="text-[1.25rem]"></Icon>
5790
</button>
@@ -62,24 +95,8 @@ let links: NavBarLink[] = navBarConfig.links.map(
6295
</div>
6396

6497
<script>
65-
function switchTheme() {
66-
if (localStorage.theme === 'dark') {
67-
document.documentElement.classList.remove('dark');
68-
localStorage.theme = 'light';
69-
} else {
70-
document.documentElement.classList.add('dark');
71-
localStorage.theme = 'dark';
72-
}
73-
}
7498

7599
function loadButtonScript() {
76-
let switchBtn = document.getElementById("scheme-switch");
77-
if (switchBtn) {
78-
switchBtn.onclick = function () {
79-
switchTheme()
80-
};
81-
}
82-
83100
let settingBtn = document.getElementById("display-settings-switch");
84101
if (settingBtn) {
85102
settingBtn.onclick = function () {
@@ -104,38 +121,13 @@ function loadButtonScript() {
104121
loadButtonScript();
105122
</script>
106123

107-
{import.meta.env.PROD && <script is:inline define:vars={{scriptUrl: url('/pagefind/pagefind.js')}}>
108-
async function loadPagefind() {
109-
try {
110-
const response = await fetch(scriptUrl, { method: 'HEAD' });
111-
if (!response.ok) {
112-
throw new Error(`Pagefind script not found: ${response.status}`);
113-
}
114-
115-
const pagefind = await import(scriptUrl);
116-
117-
await pagefind.options({
118-
excerptLength: 20
119-
});
120-
121-
window.pagefind = pagefind;
122-
123-
document.dispatchEvent(new CustomEvent('pagefindready'));
124-
console.log('Pagefind loaded and initialized successfully, event dispatched.');
125-
} catch (error) {
126-
console.error('Failed to load Pagefind:', error);
127-
window.pagefind = {
128-
search: () => Promise.resolve({ results: [] }),
129-
options: () => Promise.resolve(),
130-
};
131-
document.dispatchEvent(new CustomEvent('pagefindloaderror'));
132-
console.log('Pagefind load error, event dispatched.');
124+
<style>
125+
html[data-theme-mode="light"] .theme-icon-dark,
126+
html[data-theme-mode="light"] .theme-icon-auto,
127+
html[data-theme-mode="dark"] .theme-icon-light,
128+
html[data-theme-mode="dark"] .theme-icon-auto,
129+
html[data-theme-mode="auto"] .theme-icon-light,
130+
html[data-theme-mode="auto"] .theme-icon-dark {
131+
display: none;
133132
}
134-
}
135-
136-
if (document.readyState === 'loading') {
137-
document.addEventListener('DOMContentLoaded', loadPagefind);
138-
} else {
139-
loadPagefind();
140-
}
141-
</script>}
133+
</style>

src/components/Search.svelte

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,24 @@
11
<script lang="ts">
22
import I18nKey from "@i18n/i18nKey";
33
import { i18n } from "@i18n/translation";
4-
import Icon from "@iconify/svelte";
4+
import Icon, { addIcon } from "@iconify/svelte";
5+
import materialIcons from '@iconify-json/material-symbols/icons.json';
6+
import fa6Icons from '@iconify-json/fa6-solid/icons.json';
7+
// Preload icons to avoid CDN requests
8+
if (materialIcons.icons['search']) {
9+
addIcon('material-symbols:search', {
10+
body: materialIcons.icons['search'].body,
11+
width: materialIcons.width,
12+
height: materialIcons.height
13+
});
14+
}
15+
if (fa6Icons.icons['chevron-right']) {
16+
addIcon('fa6-solid:chevron-right', {
17+
body: fa6Icons.icons['chevron-right'].body,
18+
width: fa6Icons.width,
19+
height: fa6Icons.height
20+
});
21+
}
522
import { url } from "@utils/url-utils.ts";
623
import { onMount } from "svelte";
724
import type { SearchResult } from "@/global";
@@ -123,6 +140,21 @@ onMount(() => {
123140
}
124141
}, 2000); // Adjust timeout as needed
125142
}
143+
144+
// Wire up the search elements rendered in Navbar.astro
145+
const desktopInput = document.getElementById('search-input-desktop') as HTMLInputElement;
146+
const mobileButton = document.getElementById('search-switch');
147+
if (desktopInput) {
148+
desktopInput.addEventListener('input', (e) => {
149+
keywordDesktop = (e.target as HTMLInputElement).value;
150+
});
151+
desktopInput.addEventListener('focus', () => {
152+
search(keywordDesktop, true);
153+
});
154+
}
155+
if (mobileButton) {
156+
mobileButton.onclick = togglePanel;
157+
}
126158
});
127159
128160
$: if (initialized && keywordDesktop) {
@@ -138,24 +170,6 @@ $: if (initialized && keywordMobile) {
138170
}
139171
</script>
140172

141-
<!-- search bar for desktop view -->
142-
<div id="search-bar" class="hidden lg:flex transition-all items-center h-11 mr-2 rounded-lg
143-
bg-black/[0.04] hover:bg-black/[0.06] focus-within:bg-black/[0.06]
144-
dark:bg-white/5 dark:hover:bg-white/10 dark:focus-within:bg-white/10
145-
">
146-
<Icon icon="material-symbols:search" class="absolute text-[1.25rem] pointer-events-none ml-3 transition my-auto text-black/30 dark:text-white/30"></Icon>
147-
<input placeholder="{i18n(I18nKey.search)}" bind:value={keywordDesktop} on:focus={() => search(keywordDesktop, true)}
148-
class="transition-all pl-10 text-sm bg-transparent outline-0
149-
h-full w-40 active:w-60 focus:w-60 text-black/50 dark:text-white/50"
150-
>
151-
</div>
152-
153-
<!-- toggle btn for phone/tablet view -->
154-
<button on:click={togglePanel} aria-label="Search Panel" id="search-switch"
155-
class="btn-plain scale-animation lg:!hidden rounded-lg w-11 h-11 active:scale-90">
156-
<Icon icon="material-symbols:search" class="text-[1.25rem]"></Icon>
157-
</button>
158-
159173
<!-- search panel -->
160174
<div id="search-panel" class="float-panel float-panel-closed search-panel absolute md:w-[30rem]
161175
top-20 left-4 md:left-[unset] right-4 shadow-2xl rounded-2xl p-2">

src/layouts/Layout.astro

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,18 @@ const bannerOffset =
105105
/>
106106
))}
107107

108+
{import.meta.env.PROD && (
109+
<link rel="modulepreload" href={url('/pagefind/pagefind.js')} />
110+
)}
111+
108112
<!-- Set the theme before the page is rendered to avoid a flash -->
109113
<script is:inline define:vars={{DEFAULT_THEME, LIGHT_MODE, DARK_MODE, AUTO_MODE, BANNER_HEIGHT_EXTEND, PAGE_WIDTH, configHue}}>
110114
// Load the theme from local storage
111115
const theme = localStorage.getItem('theme') || DEFAULT_THEME;
116+
117+
// Set data attribute for theme icon display
118+
document.documentElement.setAttribute('data-theme-mode', theme);
119+
112120
switch (theme) {
113121
case LIGHT_MODE:
114122
document.documentElement.classList.remove('dark');
@@ -133,6 +141,43 @@ const bannerOffset =
133141
offset = offset - offset % 4;
134142
document.documentElement.style.setProperty('--banner-height-extend', `${offset}px`);
135143
</script>
144+
145+
{import.meta.env.PROD && <script is:inline define:vars={{scriptUrl: url('/pagefind/pagefind.js')}}>
146+
async function loadPagefind() {
147+
try {
148+
const response = await fetch(scriptUrl, { method: 'HEAD' });
149+
if (!response.ok) {
150+
throw new Error(`Pagefind script not found: ${response.status}`);
151+
}
152+
153+
const pagefind = await import(scriptUrl);
154+
155+
await pagefind.options({
156+
excerptLength: 20
157+
});
158+
159+
window.pagefind = pagefind;
160+
161+
document.dispatchEvent(new CustomEvent('pagefindready'));
162+
console.log('Pagefind loaded and initialized successfully, event dispatched.');
163+
} catch (error) {
164+
console.error('Failed to load Pagefind:', error);
165+
window.pagefind = {
166+
search: () => Promise.resolve({ results: [] }),
167+
options: () => Promise.resolve(),
168+
};
169+
document.dispatchEvent(new CustomEvent('pagefindloaderror'));
170+
console.log('Pagefind load error, event dispatched.');
171+
}
172+
}
173+
174+
if (document.readyState === 'loading') {
175+
document.addEventListener('DOMContentLoaded', loadPagefind);
176+
} else {
177+
loadPagefind();
178+
}
179+
</script>}
180+
136181
<style define:vars={{
137182
configHue,
138183
'page-width': `${PAGE_WIDTH}rem`,

0 commit comments

Comments
 (0)