目录
1831 字
9 分钟
一次博客性能与多端体验的深度优化实战

引言#

最近给本站做了一次「体检」:从构建产物、运行时行为、多端适配三个维度完整审计了一遍,然后按优先级实施优化。最终把每页首屏 JS 从约 700KB 降到约 110KB,修复了三个隐藏 bug(其中一个还是搜索功能的死循环),并顺手补齐了 PWA、SEO 和浏览器回退方案。

这篇文章记录整个过程:审计发现了什么、踩了哪些坑、最后沉淀了什么。文中所有数据均来自本站的真实构建产物与自动化测试,涉及隐私的配置(统计项目 ID、代理服务地址等)均已隐去。

一、审计:先量化,再动手#

优化的第一步不是改代码,而是拿到可信的数据。我用三把尺子:

  1. 构建产物分析:直接解包 dist 目录,按体积排序 JS/CSS/字体/图片,列出每个页面实际引用的资源
  2. 运行时观测:用 Playwright 在真实浏览器里加载页面,记录网络请求、控制台错误、页面异常
  3. 多断点走查:在 320–1920 共九个宽度下检查横向溢出与布局行为

审计结果触目惊心——最严重的是这张表:

资源体积加载时机问题
Mermaid 图表核心约 587KB每页强制加载首页和无图表文章白付代价
PhotoSwipe 灯箱约 46KB每页加载只有点开图片才需要
搜索索引运行时约 250KB每页加载应该按需
主题 CSS约 251KB每页加载存在重复引入的隐患
每页 JS 合计约 700KB移动端 4G 下体验差

另外还有两个功能性发现:

  • 搜索在部分部署链路下会失效:构建命令里没有包含搜索索引生成步骤,且索引加载器存在逻辑问题
  • 小屏溢出:欢迎通知在 ≤344px 宽的设备上会超出屏幕

二、三个值得记录的隐藏 bug#

Bug 1:一个 glob 悄悄改变了整个打包行为#

审计时发现一个诡异现象:项目里没有任何地方 import 主题样式文件,但构建产物里却有它们。排查半天,元凶是一行图片加载代码:

// 优化前:把整个 src 目录都变成了模块图的一部分
const files = import.meta.glob("../../**", { import: "default" });

这行代码本意是动态引入图片,但 ../../**src所有文件——包括所有 CSS 和 Stylus——都卷进了依赖图。主题样式之所以「莫名其妙」出现在产物里,全靠这个副作用。

修复方式是把 glob 收窄到真正的图片目录,并把样式改为显式导入:依赖关系应该写清楚,而不是靠巧合。

Bug 2:Svelte effect 的自失效死循环#

搜索组件原本用 $effect 实现「输入即搜」,但在运行时疯狂刷错误(effect_update_depth_exceeded)。原因是 effect 里调用的 search() 函数读了自己写的状态:

$effect(() => {
if (initialized && keyword) search(keyword, true);
});
async function search(keyword) {
// ...
result = searchResults; // 写入新数组
showPanel(result.length > 0); // 又读取 result.length
}

每次搜索都把 result 换成新数组引用,而 effect 又依赖 result,于是触发自己重新执行——死循环。修复方式是把搜索改成事件驱动(oninput 处理器直接调用),并在索引就绪后补跑未决查询。

教训$effect 里调用的函数如果会读写同一份响应式状态,先停下来想清楚依赖边界;交互触发的逻辑用事件处理器往往更直白。

Bug 3:模块级 import() 是「伪懒加载」#

第一次「优化」PhotoSwipe 时,我把静态导入改成了模块级动态导入:

let lightboxModule = import("photoswipe/lightbox"); // 看起来懒,其实立即发起请求

结果验证脚本当场抓包:首页依然请求了 PhotoSwipe。原因是 import() 在调用瞬间就发起网络请求,跟 await 与否无关。真正的懒加载必须把 import() 放进「用户实际需要」的回调里——最终方案是:首次点击文章图片时才加载并初始化灯箱。

教训:懒加载要验证到网络面板这一层,「改成动态导入」不等于「按需加载」。

三、优化清单#

按收益和风险分成三批实施:

P0(收益最大、风险低)

  • Mermaid 改为「页面含图表才加载」,每页直接省下约 587KB
  • PhotoSwipe 改为「点击图片才加载」
  • 修复通知溢出与 viewport 配置
  • 构建命令补上搜索索引生成,并让索引在首次搜索交互时才加载
  • 主题色补充 @supports 静态回退 + 显式声明浏览器目标

P1(移动端体验)

  • 字体改为仅打包 latin 子集(中文交给系统字体栈)
  • 封面图统一转 webp、删除 539KB 的无引用死资产、favicon 从 74KB 压到 1.7KB
  • 1280–1535 分辨率下目录改为折叠抽屉(此前直接不可见);移动端新增目录抽屉与回到顶部按钮
  • 悬浮按钮/面板避开 iPhone 安全区,高度改用 dvh 兼容地址栏伸缩
  • prefers-reduced-motion 支持、收敛全局过渡动画
  • 补 og / theme-color / apple-touch-icon 等 SEO 与分享元信息

P2(增强)

  • PWA:manifest + Service Worker(静态资源缓存优先、HTML 网络优先),弱网可读
  • AI 助手组件降级为 idle 水合,不阻塞首屏
  • 新增 Playwright 响应式冒烟测试接入 CI,PR 自动跑

四、结果对比#

指标优化前优化后
每页首屏 JS约 700KB(gzip 约 200KB)约 110KB(gzip 约 45KB)
Mermaid(587KB)每页强制加载仅含图表的页面按需加载
PhotoSwipe(46KB)每页加载首次点击图片才加载
搜索索引(250KB)每页加载且可能缺失首次搜索才加载,构建必生成
首页封面图4 张 eager JPG1 张 eager + webp
字体全部语言子集latin 子集
搜索功能死循环 bug正常,实测 4 条结果

所有改动通过三层验证后才收工:

  • 类型检查:0 错误
  • 28 项运行时验证:懒加载是否生效、搜索全链路、字体子集、PWA 注册、SEO meta 逐项断言——全部通过
  • 45 项响应式断言:九个宽度 × 五个路由,无横向溢出、无页面错误——全部通过

五、可复用的经验#

  1. 先量化再优化:解包构建产物 + 网络面板抓包,比任何猜测都准确
  2. 懒加载三要素import() 放进事件回调、产物里确认 chunk 分离、运行时抓包确认没被预取
  3. effect 慎用:响应式 effect 里别读自己写的数据,交互逻辑优先用事件处理器
  4. 依赖关系要显式:glob 引入的隐性依赖迟早反噬,改为显式导入后整个构建都变可预测
  5. 兼容性要有下限:明确声明目标浏览器,再给新特性配回退,两头都安心
  6. 把验证固化成脚本:优化完不是终点,CI 里的断言才是防回归的护栏

这次优化的全部改动和验证脚本都已开源在仓库里,感兴趣可以翻 commit 记录。如果你也在维护 Astro 博客,希望这份实战记录能帮你少踩几个坑。

一次博客性能与多端体验的深度优化实战
https://www.hehonglei.cn/posts/blog-performance-optimization-2026/
作者
Honglei He
发布于
2026-08-19
许可协议
CC BY-NC-SA 4.0