结构化改革:一次 WordPress 资源加载顺序优化
打开网站时,先看到的应该是页面的”骨架”和背景,而不是等一堆大图刷完才慢吞吞冒出来的背景色块。本文记录一次针对 WordPress 主站(自魔改主题 fox009-wisdom)的资源加载顺序排查与优化:从”图片都加载完了背景才出现”的现象出发,定位到一个看起来正确、实际完全无效的 defer 写法,最终把资源按”越大越靠后”重新分级,让动态背景与 Mermaid 图表抢在正文大图之前加载。文末另附一段番外,记录同一段 Mermaid 渲染代码此前”同毫秒重复 id 导致图表叠在一起”的修复。
起因:图片都加载完了,背景才慢慢浮现
站点的视觉基调是一层 WebGL 动态水彩背景,由 backimggene.js 驱动,配合 assets/webGL/ 下的两个着色器 shader.vert / shader.frag。它本该是最早出现的东西——毕竟它就画在 <canvas> 上、位于 <body> 最开头。
但实际体验是:页面结构出现后,先是一张张正文图片陆续刷出来,背景却要等到最后才慢慢浮现。更糟的是,在网速一般的时候,页面会明显”卡住”一下,样式和结构都迟迟不能交互。
这显然不合理。背景的代码只有 16.8KB,着色器合计不过 7KB,凭什么排在几 MB 的图片后面?
定位:当前加载顺序还原
直接抓线上 HTML 逐段核对,得到优化前的真实顺序:
<head>(按出现顺序)
- MathJax
mathjax/tex-chtml.js——async,约 779KB ,每页都加载 - 五个小脚本(分享、背景开关、彩蛋、侧栏、背景面板),均为
defer backimggene.js——type="module"(等价于 defer)wp_head()注入:大量内联样式 → 主题 CSS →countdown_js→ jQuery
<body>
<canvas>+ 背景设置面板 → 头部 → 正文(图片随解析 立即请求 ,首图还带fetchpriority="high")→ 侧栏 → 页脚
优化前的加载顺序:正文图片先于背景,3.2MB 的 mermaid 阻塞了解析,背景排在最后。
元凶:一个「看起来对、实际无效」的 defer
问题核心在 functions.php 里加载 Mermaid 的那几行:
wp_enqueue_script('mermaid-js', '/wp-content/.../mermaid.min.js', array(), null, true);
wp_script_add_data('mermaid-js', 'defer', true);
看起来给 Mermaid 加了 defer,理应不阻塞。但抓到的线上 HTML 里,这个 <script> 根本没有 defer 属性。
原因是:当前版本的 WordPress 用 strategy 键来控制脚本延迟加载,wp_script_add_data($handle, 'defer', true) 这种旧写法已经无效。 判断依据就在核心文件里:
wp-includes/class-wp-scripts.php的get_eligible_loading_strategy()读取的也是strategy。
正确的写法是:
wp_enqueue_script(
'mermaid-js',
'/wp-content/.../mermaid.min.js',
array(),
null,
array('in_footer' => false, 'strategy' => 'defer')
);
于是,那个 3.2MB 的 Mermaid 实际是一个普通的阻塞脚本。浏览器解析到页脚时被它卡住,必须等它下载并执行完,DOMContentLoaded 才会触发;而 backimggene.js 是 module,必须等解析结束才执行——也就意味着背景要等 Mermaid 下完才开始去取着色器。这就是”背景最后才出现”的真正原因。
顺带一提,同一个问题也出现在 countdown_js 上(同样 defer 失效),只是它只有 1.5KB,影响可忽略。
目标:把资源按”越大越靠后”重新分级
明确原则后,目标优先级如下:
| 优先级 | 资源 | 手段 |
|---|---|---|
| 关键路径 | HTML 结构 + 主题 CSS | 保持渲染阻塞,不动 |
| 高 | 动态背景 JS + 着色器 | 预取着色器,让背景在解析结束时即可绘制 |
| 中 | Mermaid、各小交互脚本 | 统一切换为真正生效的 defer |
| 低(延迟) | 正文图 / 特色图 | loading=lazy + fetchpriority=low |
动手:三处关键改动
1. 修好 Mermaid 的 defer,并改为按需加载
除了改用 strategy 键,还加了一个判断:只有单篇文章内容里真的出现 mermaid 时才加载。否则像首页、归档页这种没有图表的页面,也要白白下载 3.2MB。
add_action('wp_enqueue_scripts', 'load_mermaid_js');
function load_mermaid_js() {
// 仅当单篇内容中出现 mermaid 时才加载
if ( ! is_singular() ) {
return;
}
$post = get_post();
if ( ! $post || false === strpos( $post->post_content, 'mermaid' ) ) {
return;
}
// head + defer:先于正文图片被发现,但不阻塞解析与渲染
wp_enqueue_script(
'mermaid-js',
'/wp-content/themes/fox009-wisdom/assets/js/mermaid.min.js',
array(),
null,
array('in_footer' => false, 'strategy' => 'defer')
);
// ……原有的 mermaid.initialize / mermaid.run 内联脚本保持不变
}
放在 <head> 且 defer,让它在正文图片被解析之前就开始下载,同时又不阻塞渲染;按需加载则让绝大多数页面彻底告别这 3.2MB。
2. 让动态背景抢在图片前面
背景慢的另一半原因,是着色器只在 module 执行时(即解析结束)才开始 fetch,那时正文图片早已占据带宽。解决办法是在 <head> 里预取这两个小文件:
<link rel="preload" href="./wp-content/themes/fox009-wisdom/assets/webGL/shader.vert"
as="fetch" crossorigin fetchpriority="high">
<link rel="preload" href="./wp-content/themes/fox009-wisdom/assets/webGL/shader.frag"
as="fetch" crossorigin fetchpriority="high">
并给背景脚本本身加上同样的优先级提示:
<script src="./wp-content/themes/fox009-wisdom/assets/js/backimggene.js"
type="module" fetchpriority="high"></script>
着色器合计只有约 7KB,会在 CSS 之后、图片之前并行取回。等 module 一执行,着色器已经在缓存里,背景几乎瞬间就能绘制出来。
小细节:
crossorigin必须加上,且要与 JS 里fetch()的默认凭据模式一致,否则预取与实际请求无法命中同一份缓存,等于白下载一遍。
3. 把图片降级为惰性加载
最后,让正文图片和特色图退到队尾。通过两个过滤器强制加上 loading="lazy" 与 fetchpriority="low":
// 正文(the_content)内的图片
function fox009_wisdom_deprioritize_content_images( $image ) {
if ( false === stripos( $image, '<img' ) || ! class_exists( 'WP_HTML_Tag_Processor' ) ) {
return $image;
}
$processor = new WP_HTML_Tag_Processor( $image );
if ( $processor->next_tag( 'IMG' ) ) {
$processor->set_attribute( 'loading', 'lazy' );
$processor->set_attribute( 'fetchpriority', 'low' );
}
return $processor->get_updated_html();
}
add_filter( 'wp_content_img_tag', 'fox009_wisdom_deprioritize_content_images', 20 );
// 特色图/缩略图(the_post_thumbnail),仅针对文章配图,不影响站点 Logo
function fox009_wisdom_deprioritize_attachment_images( $attr ) {
if ( isset( $attr['class'] ) && false !== strpos( $attr['class'], 'wp-post-image' ) ) {
$attr['loading'] = 'lazy';
$attr['fetchpriority'] = 'low';
}
return $attr;
}
add_filter( 'wp_get_attachment_image_attributes', 'fox009_wisdom_deprioritize_attachment_images', 20 );
其中 loading="lazy" 还顺带解决了 WordPress 自动给首图加 fetchpriority="high" 的问题——因为二者互斥,一旦标为懒加载,核心就会跳过首图的高优先级处理。
效果:优化前后的加载顺序
优化后:结构优先,背景紧随其后,Mermaid 不阻塞,大图靠后。
- 背景不再被 3.2MB 的 Mermaid 挡在后面,解析结束瞬间即可绘制;
- Mermaid 从”每页阻塞加载 3.2MB”变成”有图页面才加载,且不阻塞渲染”;
- 正文图片统一降为低优先级懒加载,把带宽让给结构与背景。
番外:当 Mermaid 图表”叠”在一起
既然这次优化绕不开 Mermaid,就顺带记录同一段渲染代码此前踩过的另一个坑。那次的修复提交是「mermaid修复:修复了同毫秒重复 id 导致的图合并/空白问题」,症状和加载顺序无关,却同样迷惑人。
现象
一篇文章里如果有很多张图表(尤其是每张图里还带若干 subgraph 子图),会出现诡异的结果:有的图互相重叠、合并成一张,有的干脆渲染成空白,刷新几次时好时坏。单张图的小页面却一切正常。
根因:把毫秒时间戳当成了唯一 id
Mermaid 在渲染时会为每张图生成一批内部标识符,用来关联 SVG 里的 <defs>、clipPath、marker 等定义。问题在于,它生成标识符的部分路径直接取当前时间戳——在压缩代码里能看到类似 this.next = a ? () => this.count++ : () => Date.now() 的写法,没有显式序列时就用 Date.now() 兜底。
而原来主题的渲染循环,是在 forEach 里对每一张图单独调用一次 mermaid.run():
const elements = document.querySelectorAll(".mermaid");
elements.forEach(function(el) {
// ……规整文本
el.textContent = rawText;
mermaid.run({
nodes: [el] // 每张图各跑一次
}).catch(function(err) {
console.error("【Mermaid 详细错误信息】:", err);
});
});
这些同步调用发生在同一个毫秒内,于是多张图拿到了相同的 id。id 一旦冲突,不同图表就共用起同一批 SVG 定义,结果就是”画到一起、叠起来”,或者定义互相覆盖后变成空白。图越多、子图越多,撞 id 的概率越大——这正好解释了为什么单图页面正常、多图长文才出问题。
修复:收集后一次性渲染
把散落在循环里的单次调用收拢,等所有文本规整完毕后,对整批节点只调用一次 mermaid.run():
const elements = document.querySelectorAll(".mermaid");
elements.forEach(function(el) {
// ……规整文本
el.textContent = rawText;
});
mermaid.run({
nodes: elements // 整批一次渲染
}).catch(function(err) {
console.error("【Mermaid 详细错误信息】:", err);
});
同一批次内,Mermaid 会统一分配标识符,毫秒级的时间戳不再有机会撞车,图表便各归各位。
教训
- 只要第三方库会生成 DOM/SVG 的
id,就要警惕”同一 tick 内循环单次调用” 。要么批量调用,要么在调用之间留出真实的时间间隔,否则以时间戳为种子的 id 极容易重复。 - 这类 bug 的表象(图叠在一起、空白)和真正的成因(id 冲突)看起来毫不相干,排查时不妨先去压缩源码里搜一搜
Date.now()、Math.random()这类”随机来源”。
验证方法
如果你也想复现这套排查,可以不用本地起 PHP:
- 直接在浏览器 DevTools 的 Network 面板按优先级排序,确认着色器请求早于图片批量请求,Mermaid 带
defer且无 parser-block; - 或用命令行抓线上 HTML,用
start_index分段读取<head>与页脚输出,核对每个<script>是否真的带上了defer; - 重点确认三件事:Mermaid 带
defer且不再每页出现;首图不再有fetchpriority="high";<head>里出现两条着色器preload。
踩坑与注意事项
wp_script_add_data($h, 'defer', true)并非万能 。不同 WordPress 版本对延迟加载的支持方式不同,务必以实际输出的 HTML 为准——属性没出现,就是没生效。preload as="fetch"必须与fetch()的凭据模式匹配 ,否则会重复下载,反而更慢。- 不要一次性重排所有资源 。本次只动”背景 + Mermaid + 图片”三处,保持 CSS 依旧渲染阻塞,避免结构本身变慢。
结语
性能优化里最值得警惕的,往往不是”没做优化”,而是”以为做了优化”。这次最大的教训,是一个语法完全正确、却在当前版本里毫无效果的 defer,悄悄把 3.2MB 的阻塞脚本留在了关键路径上。把顺序理顺之后,网站终于做到了它该有的样子:先结构,再背景,最后才是大图。
作者信息
NGC13009(limitless) 2026-09-26
