'性能优化'

This commit is contained in:
eric
2026-03-23 23:19:51 -05:00
parent 0c1fc6e67b
commit 0befec0d5f
3 changed files with 116 additions and 34 deletions

View File

@@ -80,5 +80,6 @@
"typescript": "^4.1.0",
"webpack": "^5.78.0",
"webpack-obfuscator": "^3.5.1"
}
},
"packageManager": "pnpm@10.25.0+sha1.2cfb3ab644446565c127f58165cc76368c9c920b"
}

View File

@@ -187,6 +187,11 @@
word-wrap: break-word;
overflow-wrap: break-word;
/* 目录 scrollIntoView 时预留固定顶栏高度(与 .book_header_div 一致) */
[data-chunk-index] {
scroll-margin-top: 88px;
}
h1 {
font-size: 48px !important;
font-weight: 800;
@@ -574,6 +579,10 @@
max-width: 800PX;
margin: 0 auto;
[data-chunk-index] {
scroll-margin-top: 56PX;
}
h1 {
font-size: 28PX !important;
margin: 36PX 0 14PX;

View File

@@ -3,7 +3,7 @@ import { View, Image } from "@tarojs/components";
import Taro, { useRouter } from "@tarojs/taro";
import "./index.scss";
import { AtButton, AtDrawer, AtIcon } from "taro-ui";
import React, { useEffect, useState, useRef, useCallback } from "react";
import React, { useEffect, useState, useRef, useCallback, useLayoutEffect } from "react";
import { chunks as infoChunks, headers as infoHeaders } from "./info.md";
import hackrobot from "../../images/hackrobot.jpg";
@@ -13,11 +13,15 @@ function getPb() {
return PB ? new PB(PB_URL) : null;
}
const LazyChunk = React.memo(({ html, estimatedHeight, forceVisible }) => {
const LazyChunk = React.memo(({ html, estimatedHeight, forceVisible, chunkIndex }) => {
const [visible, setVisible] = useState(forceVisible);
const ref = useRef(null);
const show = forceVisible || visible;
useEffect(() => {
if (forceVisible) setVisible(true);
}, [forceVisible]);
useEffect(() => {
if (forceVisible) return;
if (visible) return;
@@ -37,9 +41,23 @@ const LazyChunk = React.memo(({ html, estimatedHeight, forceVisible }) => {
}, [forceVisible, visible]);
if (show) {
return <div className="chunk-enter" dangerouslySetInnerHTML={{ __html: html }} />;
const animate = !forceVisible;
return (
<div
data-chunk-index={chunkIndex}
className={animate ? "chunk-enter" : undefined}
dangerouslySetInnerHTML={{ __html: html }}
/>
);
}
return <div ref={ref} className="chunk-placeholder" style={{ minHeight: estimatedHeight }} />;
return (
<div
ref={ref}
data-chunk-index={chunkIndex}
className="chunk-placeholder"
style={{ minHeight: estimatedHeight }}
/>
);
});
const Book = () => {
@@ -49,7 +67,11 @@ const Book = () => {
const [paidType, setPaidType] = useState(null);
const [userId, setUserId] = useState(Taro.getStorageSync("userId") || null);
const [loading, setLoading] = useState(true);
const [forceShowAll, setForceShowAll] = useState(false);
/** 目录跳转时至少渲染到该索引的 chunknull 表示仅首块 + 懒加载 */
const [visibleUntilIndex, setVisibleUntilIndex] = useState(null);
/** 每次点击目录递增,驱动 useLayoutEffect 在 DOM 提交后滚动(不依赖 visibleUntilIndex 是否变化) */
const [jumpSeq, setJumpSeq] = useState(0);
const pendingJumpIndexRef = useRef(null);
const [readProgress, setReadProgress] = useState(0);
const [showBackTop, setShowBackTop] = useState(false);
const [bookData, setBookData] = useState(null);
@@ -182,45 +204,94 @@ const Book = () => {
if (e.target.tagName === "IMG") e.target.classList.add("loaded");
};
el.addEventListener("load", onImgLoad, true);
let raf = 0;
let pending = false;
const checkCached = () => {
pending = false;
el.querySelectorAll("img").forEach(img => {
if (img.complete) img.classList.add("loaded");
});
};
const mo = new MutationObserver(checkCached);
const scheduleCheck = () => {
if (pending) return;
pending = true;
raf = requestAnimationFrame(checkCached);
};
const mo = new MutationObserver(() => scheduleCheck());
mo.observe(el, { childList: true, subtree: true });
checkCached();
scheduleCheck();
return () => {
cancelAnimationFrame(raf);
el.removeEventListener("load", onImgLoad, true);
mo.disconnect();
};
}, [contentLoading]);
const handleJump = useCallback((index) => {
setForceShowAll(true);
setshow(false);
const run = () => {
const root = contentRef.current;
if (!root) return;
const h1s = root.querySelectorAll("h1");
const el = h1s[index];
if (el) {
try {
el.scrollIntoView({ behavior: "smooth", block: "start" });
} catch (_) {
const top = el.getBoundingClientRect().top + (window.pageYOffset || document.documentElement.scrollTop) - 60;
window.scrollTo({ top: Math.max(0, top), behavior: "smooth" });
}
}
};
run();
setTimeout(run, 100);
setTimeout(run, 300);
setTimeout(run, 600);
const getMarkdownRoot = useCallback(() => {
const el = contentRef.current;
if (el && typeof el.querySelector === "function") return el;
if (typeof document !== "undefined") {
return document.querySelector(".Book .markdown-body");
}
return null;
}, []);
const scrollToChapterIndex = useCallback((index, attempt = 0) => {
const root = getMarkdownRoot();
if (!root) return;
const el = root.querySelector(`[data-chunk-index="${index}"]`);
if (el?.classList.contains("chunk-placeholder") && attempt < 48) {
requestAnimationFrame(() => scrollToChapterIndex(index, attempt + 1));
return;
}
const h1list = root.querySelectorAll("h1");
const target = el || h1list[index];
if (!target) return;
try {
target.scrollIntoView({ block: "start", behavior: "auto" });
} catch (_) {
const rect = target.getBoundingClientRect();
const st = window.pageYOffset || document.documentElement.scrollTop || 0;
const dest = Math.max(0, rect.top + st - 60);
try {
window.scrollTo({ top: dest, behavior: "auto" });
} catch (__) {
window.scrollTo(0, dest);
}
}
}, [getMarkdownRoot]);
const handleJump = useCallback((rawIndex) => {
const index =
typeof rawIndex === "number" && Number.isFinite(rawIndex)
? rawIndex
: parseInt(String(rawIndex), 10);
if (!Number.isFinite(index) || index < 0) return;
pendingJumpIndexRef.current = index;
setVisibleUntilIndex(prev => Math.max(prev ?? -1, index));
setshow(false);
setJumpSeq(s => s + 1);
}, []);
useLayoutEffect(() => {
if (jumpSeq === 0) return;
const index = pendingJumpIndexRef.current;
if (index === null || index === undefined) return;
const run = () => scrollToChapterIndex(index);
run();
const t1 = setTimeout(run, 50);
const t2 = setTimeout(run, 200);
const t3 = setTimeout(run, 450);
requestAnimationFrame(() => {
requestAnimationFrame(run);
});
return () => {
clearTimeout(t1);
clearTimeout(t2);
clearTimeout(t3);
};
}, [jumpSeq, scrollToChapterIndex]);
const submitPayForm = (xorpayUrl, xorpayParams) => {
const form = document.createElement("form");
form.method = "POST";
@@ -301,16 +372,17 @@ const Book = () => {
<View className="loading-text">...</View>
</View>
) : (
<View ref={contentRef} className="markdown-body">
<div ref={contentRef} className="markdown-body">
{activeChunks.map((chunk, i) => (
<LazyChunk
key={`${isPaid}-${i}`}
chunkIndex={i}
html={chunk.html}
estimatedHeight={chunk.estimatedHeight}
forceVisible={forceShowAll || i === 0}
forceVisible={(visibleUntilIndex !== null && i <= visibleUntilIndex) || i === 0}
/>
))}
</View>
</div>
)}
<AtDrawer