目录
引言
最近给本站做了一次「体检」:从构建产物、运行时行为、多端适配三个维度完整审计了一遍,然后按优先级实施优化。最终把每页首屏 JS 从约 700KB 降到约 110KB,修复了三个隐藏 bug(其中一个还是搜索功能的死循环),并顺手补齐了 PWA、SEO 和浏览器回退方案。
这篇文章记录整个过程:审计发现了什么、踩了哪些坑、最后沉淀了什么。文中所有数据均来自本站的真实构建产物与自动化测试,涉及隐私的配置(统计项目 ID、代理服务地址等)均已隐去。
一、审计:先量化,再动手
优化的第一步不是改代码,而是拿到可信的数据。我用三把尺子:
- 构建产物分析:直接解包 dist 目录,按体积排序 JS/CSS/字体/图片,列出每个页面实际引用的资源
- 运行时观测:用 Playwright 在真实浏览器里加载页面,记录网络请求、控制台错误、页面异常
- 多断点走查:在 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 JPG | 1 张 eager + webp |
| 字体 | 全部语言子集 | latin 子集 |
| 搜索功能 | 死循环 bug | 正常,实测 4 条结果 |
所有改动通过三层验证后才收工:
- 类型检查:0 错误
- 28 项运行时验证:懒加载是否生效、搜索全链路、字体子集、PWA 注册、SEO meta 逐项断言——全部通过
- 45 项响应式断言:九个宽度 × 五个路由,无横向溢出、无页面错误——全部通过
五、可复用的经验
- 先量化再优化:解包构建产物 + 网络面板抓包,比任何猜测都准确
- 懒加载三要素:
import()放进事件回调、产物里确认 chunk 分离、运行时抓包确认没被预取 - effect 慎用:响应式 effect 里别读自己写的数据,交互逻辑优先用事件处理器
- 依赖关系要显式:glob 引入的隐性依赖迟早反噬,改为显式导入后整个构建都变可预测
- 兼容性要有下限:明确声明目标浏览器,再给新特性配回退,两头都安心
- 把验证固化成脚本:优化完不是终点,CI 里的断言才是防回归的护栏
这次优化的全部改动和验证脚本都已开源在仓库里,感兴趣可以翻 commit 记录。如果你也在维护 Astro 博客,希望这份实战记录能帮你少踩几个坑。