Skip to content

Commit 8cebd6e

Browse files
perf: lazy load Twikoo, disconnect Mermaid observer, add resource hints
- Twikoo: lazy load via IntersectionObserver, cleanup event listeners - Mermaid: disconnect MutationObserver on page navigation - HeadTags: add CDN preconnect and font preload hints - Layout: change Pio to client:visible for better code splitting
1 parent 0ee7f37 commit 8cebd6e

4 files changed

Lines changed: 224 additions & 75 deletions

File tree

src/components/comment/Twikoo.astro

Lines changed: 156 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -12,71 +12,160 @@ const config = {
1212
};
1313
---
1414

15-
<div id="tcomment"></div>
16-
<script is:inline src="/scroll-protection.js"></script>
17-
<script is:inline src="/assets/js/twikoo.all.min.js"></script>
15+
<div id="twikoo-container" class="twikoo-container">
16+
<div id="tcomment"></div>
17+
</div>
18+
1819
<script is:inline define:vars={{ config }}>
19-
// 获取当前页面路径
20-
function getCurrentPath() {
21-
const pathname = window.location.pathname;
22-
return pathname.endsWith('/') && pathname.length > 1 ? pathname.slice(0, -1) : pathname;
23-
}
24-
25-
// 动态创建配置对象
26-
function createTwikooConfig() {
27-
return {
28-
...config,
29-
path: getCurrentPath(),
30-
el: '#tcomment'
31-
};
32-
}
33-
34-
// 初始化 Twikoo
35-
function initTwikoo() {
36-
if (typeof twikoo !== 'undefined') {
37-
const commentEl = document.getElementById('tcomment');
38-
if (commentEl) {
39-
commentEl.innerHTML = '';
40-
41-
const dynamicConfig = createTwikooConfig();
42-
console.log('[Twikoo] 初始化配置:', dynamicConfig);
43-
44-
twikoo.init(dynamicConfig).then(() => {
45-
console.log('[Twikoo] 初始化完成');
46-
}).catch((error) => {
47-
console.error('[Twikoo] 初始化失败:', error);
48-
});
49-
}
50-
} else {
51-
// 如果 Twikoo 未加载,稍后重试
52-
setTimeout(initTwikoo, 500);
53-
}
54-
}
55-
56-
// 页面加载时初始化
57-
document.addEventListener('DOMContentLoaded', initTwikoo);
58-
59-
// Swup 页面切换后重新初始化
60-
if (window.swup && window.swup.hooks) {
61-
window.swup.hooks.on('content:replace', function() {
62-
setTimeout(initTwikoo, 200);
63-
});
64-
} else {
65-
document.addEventListener('swup:enable', function() {
66-
if (window.swup && window.swup.hooks) {
67-
window.swup.hooks.on('content:replace', function() {
68-
setTimeout(initTwikoo, 200);
69-
});
70-
}
71-
});
72-
}
73-
74-
// 自定义事件监听
75-
document.addEventListener('mizuki:page:loaded', function() {
76-
const commentEl = document.getElementById('tcomment');
77-
if (commentEl) {
78-
console.log('[Twikoo] 通过自定义事件重新初始化');
79-
initTwikoo();
80-
}
81-
});
82-
</script>
20+
(function () {
21+
const TWIKOO_CONTAINER_ID = "twikoo-container";
22+
const SCROLL_PROTECTION_URL = "/scroll-protection.js";
23+
const TWIKOO_SCRIPT_URL = "/assets/js/twikoo.all.min.js";
24+
const ROOT_MARGIN = "200px";
25+
26+
let observer = null;
27+
let twikooLoaded = false;
28+
29+
function getCurrentPath() {
30+
const pathname = window.location.pathname;
31+
return pathname.endsWith("/") && pathname.length > 1
32+
? pathname.slice(0, -1)
33+
: pathname;
34+
}
35+
36+
function createTwikooConfig() {
37+
return {
38+
...config,
39+
path: getCurrentPath(),
40+
el: "#tcomment",
41+
};
42+
}
43+
44+
async function loadScrollProtection() {
45+
return new Promise((resolve, reject) => {
46+
if (document.getElementById("scroll-protection-loaded")) {
47+
resolve();
48+
return;
49+
}
50+
const script = document.createElement("script");
51+
script.src = SCROLL_PROTECTION_URL;
52+
script.onload = () => {
53+
const marker = document.createElement("div");
54+
marker.id = "scroll-protection-loaded";
55+
document.body.appendChild(marker);
56+
resolve();
57+
};
58+
script.onerror = reject;
59+
document.head.appendChild(script);
60+
});
61+
}
62+
63+
async function loadTwikooScript() {
64+
if (twikooLoaded || document.getElementById("twikoo-script-loaded")) {
65+
return;
66+
}
67+
await loadScrollProtection();
68+
return new Promise((resolve, reject) => {
69+
const script = document.createElement("script");
70+
script.id = "twikoo-script-loaded";
71+
script.src = TWIKOO_SCRIPT_URL;
72+
script.onload = () => {
73+
twikooLoaded = true;
74+
resolve();
75+
};
76+
script.onerror = reject;
77+
document.head.appendChild(script);
78+
});
79+
}
80+
81+
async function initTwikoo() {
82+
const commentEl = document.getElementById("tcomment");
83+
if (!commentEl) return;
84+
85+
try {
86+
await loadTwikooScript();
87+
88+
if (typeof twikoo === "undefined") {
89+
console.warn("[Twikoo] 脚本加载失败");
90+
return;
91+
}
92+
93+
commentEl.innerHTML = "";
94+
const dynamicConfig = createTwikooConfig();
95+
await twikoo.init(dynamicConfig);
96+
} catch (error) {
97+
console.error("[Twikoo] 初始化失败:", error);
98+
}
99+
}
100+
101+
function cleanup() {
102+
if (observer) {
103+
observer.disconnect();
104+
observer = null;
105+
}
106+
if (window.swup?.hooks) {
107+
window.swup.hooks.off("content:replace", initTwikoo);
108+
}
109+
}
110+
111+
function setupLazyLoad() {
112+
const container = document.getElementById(TWIKOO_CONTAINER_ID);
113+
if (!container) return;
114+
115+
observer = new IntersectionObserver(
116+
(entries) => {
117+
if (entries[0].isIntersecting) {
118+
observer.disconnect();
119+
observer = null;
120+
initTwikoo();
121+
}
122+
},
123+
{ rootMargin: ROOT_MARGIN },
124+
);
125+
126+
observer.observe(container);
127+
}
128+
129+
function setupSwupHooks() {
130+
if (window.swup?.hooks) {
131+
window.swup.hooks.on("content:replace", () => {
132+
setTimeout(setupLazyLoad, 200);
133+
});
134+
} else {
135+
document.addEventListener(
136+
"swup:enable",
137+
() => {
138+
if (window.swup?.hooks) {
139+
window.swup.hooks.on("content:replace", () => {
140+
setTimeout(setupLazyLoad, 200);
141+
});
142+
}
143+
},
144+
{ once: true },
145+
);
146+
}
147+
}
148+
149+
document.addEventListener("mizuki:page:loaded", () => {
150+
setupLazyLoad();
151+
});
152+
153+
window.addEventListener("beforeunload", cleanup);
154+
155+
if (document.readyState === "loading") {
156+
document.addEventListener("DOMContentLoaded", () => {
157+
setupLazyLoad();
158+
setupSwupHooks();
159+
});
160+
} else {
161+
setupLazyLoad();
162+
setupSwupHooks();
163+
}
164+
})();
165+
</script>
166+
167+
<style>
168+
.twikoo-container {
169+
min-height: 200px;
170+
}
171+
</style>

src/layouts/Layout.astro

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,8 +230,8 @@ if (siteConfig.font) {
230230
<!-- Music Player - 核心始终加载(当enable=true),UI通过showFloatingPlayer控制 -->
231231
<MusicPlayer client:idle />
232232

233-
<!-- Pio 看板娘组件 - 仅在启用时加载 -->
234-
{pioConfig.enable && <Pio client:idle />}
233+
<!-- Pio 看板娘组件 - 仅在进入视口时加载 -->
234+
{pioConfig.enable && <Pio client:visible />}
235235

236236
<!-- increase the page height during page transition to prevent the scrolling animation from jumping -->
237237
<div id="page-height-extend" class="hidden h-[300vh]"></div>

src/layouts/partials/HeadTags.astro

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ const keywords = siteConfig.keywords;
4444
// Favicons
4545
const favicons: Favicon[] =
4646
siteConfig.favicon.length > 0 ? siteConfig.favicon : defaultFavicons;
47+
48+
// CDN 预连接域名(用于 Mermaid 等第三方资源)
49+
const cdnDomains = [
50+
"https://cdn.jsdelivr.net",
51+
"https://unpkg.com",
52+
];
4753
---
4854

4955
<!-- SEO Meta Tags -->
@@ -103,6 +109,29 @@ const favicons: Favicon[] =
103109
))
104110
}
105111

112+
<!-- CDN 预连接 -->
113+
{
114+
cdnDomains.map((domain) => (
115+
<link rel="preconnect" href={domain} crossorigin />
116+
))
117+
}
118+
119+
<!-- 字体预加载(使用压缩后的 woff2 格式) -->
120+
<link
121+
rel="preload"
122+
href="/fonts/loli.woff2"
123+
as="font"
124+
type="font/woff2"
125+
crossorigin
126+
/>
127+
<link
128+
rel="preload"
129+
href="/fonts/ZenMaruGothic-Medium.woff2"
130+
as="font"
131+
type="font/woff2"
132+
crossorigin
133+
/>
134+
106135
<!-- Set the theme before the page is rendered to avoid a flash -->
107136
<script
108137
is:inline

src/plugins/mermaid-render-script.js

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,22 @@
5555
});
5656
}
5757

58+
// 存储 MutationObserver 实例
59+
let themeObserver = null;
60+
61+
// 清理 MutationObserver
62+
function cleanupMutationObserver() {
63+
if (themeObserver) {
64+
themeObserver.disconnect();
65+
themeObserver = null;
66+
}
67+
}
68+
5869
// 设置 MutationObserver 监听 html 元素的 class 属性变化
5970
function setupMutationObserver() {
60-
const observer = new MutationObserver((mutations) => {
71+
cleanupMutationObserver();
72+
73+
themeObserver = new MutationObserver((mutations) => {
6174
mutations.forEach((mutation) => {
6275
if (
6376
mutation.type === "attributes" &&
@@ -81,7 +94,7 @@
8194
});
8295

8396
// 开始观察 html 元素的 class 属性变化
84-
observer.observe(document.documentElement, {
97+
themeObserver.observe(document.documentElement, {
8598
attributes: true,
8699
attributeFilter: ["class"],
87100
attributeOldValue: true,
@@ -90,7 +103,9 @@
90103

91104
// 缩放平移
92105
function attachZoomControls(element, svgElement) {
93-
if (element.__zoomAttached) {return;}
106+
if (element.__zoomAttached) {
107+
return;
108+
}
94109
element.__zoomAttached = true;
95110

96111
const wrapper = document.createElement("div");
@@ -121,7 +136,9 @@
121136
controls.addEventListener("click", (ev) => {
122137
const action =
123138
ev.target.getAttribute && ev.target.getAttribute("data-action");
124-
if (!action) {return;}
139+
if (!action) {
140+
return;
141+
}
125142

126143
switch (action) {
127144
case "zoom-in":
@@ -150,7 +167,9 @@
150167
wrapper.style.touchAction = "none";
151168

152169
wrapper.addEventListener("pointerdown", (ev) => {
153-
if (ev.button !== 0) {return;} // 仅左键
170+
if (ev.button !== 0) {
171+
return;
172+
} // 仅左键
154173
isPanning = true;
155174
wrapper.setPointerCapture(ev.pointerId);
156175
startX = ev.clientX;
@@ -160,7 +179,9 @@
160179
});
161180

162181
wrapper.addEventListener("pointermove", (ev) => {
163-
if (!isPanning) {return;}
182+
if (!isPanning) {
183+
return;
184+
}
164185
const dx = ev.clientX - startX;
165186
const dy = ev.clientY - startY;
166187
tx = startTx + dx / scale; // 根据当前缩放调整灵敏度
@@ -242,6 +263,16 @@
242263
setTimeout(() => renderMermaidDiagrams(), 200);
243264
}
244265
});
266+
267+
// 页面切换前清理
268+
document.addEventListener("astro:before-swap", cleanupMutationObserver);
269+
270+
// Swup 页面切换时重新设置 Observer
271+
document.addEventListener("astro:after-swap", () => {
272+
if (themeObserver === null) {
273+
setupMutationObserver();
274+
}
275+
});
245276
}
246277

247278
async function initializeMermaid() {

0 commit comments

Comments
 (0)