我总结了 Typecho 主题里常见的 Markdown 资源按需加载方案,重点是 KaTeX highlight.js
主题会在所有页面加载:
katex.min.css/katex.min.jshighlight.min.js
这样会导致:
- 首页、分类页、非渲染的文章也发起请求
- 首屏重
- CDN 请求数增加
于是我采用按需加载:
- 没有公式就不加载 KaTeX
- 没有代码块就不加载 highlight
- 检测时排除
pre/code,避免误判 - 保证主题切换(明/暗)下高亮样式正确
首页结果:
| 状态 | 域名 | 文件 | 耗时 |
|---|---|---|---|
| 200 | www.hansjack.com | / | 59 ms |
| 200 | www.hansjack.com | result.css | 66 ms |
| 200 | www.hansjack.com | style.css | 53 ms |
| x | www.blogsclub.org | www.hansjack.com | 109 ms |
KaTeX 预判(含 pre/code 排除)
<?php
use Widget\Archive;
function hjArchiveContentContainsKatexSyntax(string $content): bool
{
if ($content === '') {
return false;
}
// 关键:先排除 pre/code,避免代码示例里的 $...$ 触发误判
$scanContent = $content;
if (strpos($scanContent, '<') !== false) {
$withoutPre = preg_replace('/<pre\b[^>]*>[\s\S]*?<\/pre>/iu', ' ', $scanContent);
if (is_string($withoutPre)) {
$scanContent = $withoutPre;
}
$withoutCode = preg_replace('/<code\b[^>]*>[\s\S]*?<\/code>/iu', ' ', $scanContent);
if (is_string($withoutCode)) {
$scanContent = $withoutCode;
}
}
if ($scanContent === '') {
return false;
}
if (strpos($scanContent, '$') !== false) {
if (preg_match('/\$\$[\s\S]+?\$\$/u', $scanContent) === 1) {
return true;
}
if (preg_match('/(?<!\\\\)\$(?!\s)(?:[^$\\\\\r\n]|\\\\.)+?(?<!\s)(?<!\\\\)\$/u', $scanContent) === 1) {
return true;
}
}
if (strpos($scanContent, '\\') !== false) {
if (preg_match('/\\\\\([\s\S]+?\\\\\)|\\\\\[[\s\S]+?\\\\\]/u', $scanContent) === 1) {
return true;
}
if (preg_match('/\\\\begin\{(?:equation|align|alignat|gather|CD)\}/u', $scanContent) === 1) {
return true;
}
}
return false;
}
function hjShouldLoadKatexAssets(Archive $archive): bool
{
$isRenderable = false;
try {
$isRenderable = $archive->is('post') || $archive->is('page');
} catch (\Throwable $e) {
$isRenderable = false;
}
if (!$isRenderable) {
return false;
}
try {
$content = trim((string) ($archive->content ?? ''));
} catch (\Throwable $e) {
$content = '';
}
return hjArchiveContentContainsKatexSyntax($content);
}highlight 预判
<?php
use Widget\Archive;
function hjShouldLoadHighlightAssets(Archive $archive): bool
{
$isRenderable = false;
try {
$isRenderable = $archive->is('post') || $archive->is('page');
} catch (\Throwable $e) {
$isRenderable = false;
}
if (!$isRenderable) {
return false;
}
try {
$content = (string) ($archive->content ?? '');
} catch (\Throwable $e) {
$content = '';
}
if ($content === '') {
return false;
}
// Markdown 渲染后一般为 <pre><code>...
return preg_match('/<pre\b[^>]*>\s*<code\b/i', $content) === 1;
}前端按需加载
function loadScriptOnce(src, key, done) {
if (!src) return done();
var selector = 'script[data-res-key="' + key + '"]';
var existing = document.querySelector(selector);
if (existing) {
if (existing.getAttribute("data-res-loaded") === "1") return done();
existing.addEventListener("load", done);
existing.addEventListener("error", done);
return;
}
var s = document.createElement("script");
s.src = src;
s.async = false;
s.setAttribute("data-res-key", key);
s.onload = function () {
s.setAttribute("data-res-loaded", "1");
done();
};
s.onerror = done;
document.head.appendChild(s);
}
function appendCssOnce(href, key) {
if (!href) return;
if (document.querySelector('link[data-css-key="' + key + '"]')) return;
var link = document.createElement("link");
link.rel = "stylesheet";
link.href = href;
link.setAttribute("data-css-key", key);
document.head.appendChild(link);
}(function () {
var containers = Array.prototype.slice.call(
document.querySelectorAll(".hj-article-content, .hj-comment-content")
);
if (!containers.length) return;
var codeBlocks = [];
containers.forEach(function (content) {
if (!content || !content.querySelectorAll) return;
var blocks = content.querySelectorAll("pre code");
for (var i = 0; i < blocks.length; i++) codeBlocks.push(blocks[i]);
});
if (!codeBlocks.length) return;
function applyHighlightTheme() {
var root = document.documentElement;
var isDark = root.classList.contains("hj-theme-dark") && !root.classList.contains("hj-theme-light");
var links = document.querySelectorAll("link[data-hljs-theme]");
for (var i = 0; i < links.length; i++) {
var theme = links[i].getAttribute("data-hljs-theme") || "";
links[i].disabled = isDark ? theme !== "dark" : theme !== "light";
}
}
function appendThemeLink(theme, href) {
if (!href) return;
if (document.querySelector('link[data-hljs-theme="' + theme + '"]')) return;
var link = document.createElement("link");
link.rel = "stylesheet";
link.href = href;
link.disabled = true;
link.setAttribute("data-hljs-theme", theme);
document.head.appendChild(link);
}
appendThemeLink("light", "/usr/themes/YourTheme/assets/vendor/highlight/github.min.css");
appendThemeLink("dark", "/usr/themes/YourTheme/assets/vendor/highlight/github-dark.min.css");
applyHighlightTheme();
loadScriptOnce("/usr/themes/YourTheme/assets/vendor/highlight/highlight.min.js", "hljs", function () {
if (!window.hljs) return;
try { window.hljs.configure({ ignoreUnescapedHTML: true }); } catch (e) {}
codeBlocks.forEach(function (code) {
if (!code) return;
if (code.dataset && code.dataset.highlighted) return;
try { window.hljs.highlightElement(code); } catch (e) {}
});
});
})();KaTeX 前端检测(排除 pre/code)
function hasKatexSyntax(text) {
if (!text) return false;
if (text.indexOf("$$") !== -1 && /\$\$[\s\S]+?\$\$/.test(text)) return true;
if (text.indexOf("\\(") !== -1 && /\\\([\s\S]+?\\\)/.test(text)) return true;
if (text.indexOf("\\[") !== -1 && /\\\[[\s\S]+?\\\]/.test(text)) return true;
if (text.indexOf("\\begin{") !== -1 && /\\begin\{(?:equation|align|alignat|gather|CD)\}/.test(text)) return true;
if (text.indexOf("$") !== -1 && /(^|[^\\])\$(?![\s$])(?:[^$\\\r\n]|\\.)+?\$(?!\$)/.test(text)) return true;
return false;
}
function readTextWithoutCode(node) {
if (!node) return "";
var sourceNode = node.cloneNode ? node.cloneNode(true) : node;
if (sourceNode.querySelectorAll) {
var ignored = sourceNode.querySelectorAll("pre, code");
for (var i = 0; i < ignored.length; i++) {
if (ignored[i] && ignored[i].parentNode) ignored[i].parentNode.removeChild(ignored[i]);
}
}
return String((sourceNode && sourceNode.textContent) || "");
}注意事项
- 检测误判最常见来源是代码块中的数学符号,务必排除
pre/code。 - 正则不要过宽,特别是
$...$,容易误伤金额、Shell 变量等文本。 - 动态加载脚本必须防重复,否则会出现多次初始化。
highlight明暗主题要跟随htmlclass 切换。- 若站点启用严格 CSP,需要给动态注入资源放行域名。
- 压缩和缓存策略要和按需加载配合,避免重复下载。
- 评论区若支持 Markdown,同样应纳入检测范围。
好巧,我也写过类似的
Astro: 优化katex,mermaid和灯箱使用
厉害,好文!
以前用Astro感觉构建时间太长就放弃了,果然还是不太会玩静态博客