笔者的博客基于 VitePress 搭建的,使用其自定义主题能力完成博客主题 @sugarat/theme 的搭建。
前段时间有群友反馈说使用主题构建后耗时增加非常明显。
前后耗时大概增加了 10 倍,过于离谱了。
断断续续的投入差不多 1 个月的时间完成了优化,效果还是很明显。
至此写篇文章记录&分享一下优化过程。
先看一下优化前后的效果
- 测试项目:笔者的博客,差不多 490 篇文章。
- 测试机器:Mac Mini (M1, 2020)
仅开启博客相关的样式能力
VitePress 默认主题 | 优化后主题 | 优化前主题 |
---|---|---|
16.38s | 20.56s | 32.36s |
对比目标 | +4.18s | +15.98s |
开启拓展能力
优化后 | 优化后 | 优化前 |
---|---|---|
RSS不开启HTML生成 + 离线搜索 | RSS + 离线搜索 | RSS + 离线搜索 |
25.70s | 30.93s | 50.85s |
+9.4s | +14.55s | +34.47s |
小结
整体提速约 3 倍:
- 只开启基础能力:额外耗时从 16s 缩短至 4s
- 拓展能力耗时:额外耗时从 34s 缩短到 10 s
问题定位
先定位耗时的位置,再想办法进行优化。
我们可以直接用 console.time
和 console.timeEnd
打印出耗时信息。
js
console.time('flag')
// 执行代码
console.timeEnd('flag') // 打印出耗时
主要关注有循环和外部调用的逻辑,在其前后加上打印耗时。
简单打了几个点,就有如下的结果咯 ⏰。
在主题入口和两个插件都有一段类似的代码逻辑,读取文件内容构造 meta 信息。
优化方式
异步操作文件
读取文件内容用于提取 frontmatter
信息,生成描述,标题等内容,会用于首页渲染。
使用 fs.promises
异步操作文件,这样可以避免阻塞进程。
js
// 原
fs.readFileSync(filePath, 'utf-8')
// 新
fs.promises.readFile(filePath, 'utf-8')
异步创建子进程
主要通过调用 git
指令获取文件最后的修改时间,用于展示文章的最后的修改时间。
原来使用的 spawnSync
,同样也是同步执行的方法。
使用 spawn + Promise
替换 spawnSync
,避免阻塞进程。
js
// 原
spawnSync('git', ['log', '-1', '--pretty="%ci"', url])
// 新
const child = spawn('git', ['log', '-1', '--pretty="%ai"', url])
使用缓存
在日志里可以发现,Vite 插件里 load
钩子在 vitepress build 时执行了2次。
因此针对会重复执行的逻辑,可以添加添加一段缓存读写的逻辑,能明显降低二次执行相关逻辑的时间。
时间的获取使用 Map
缓存文件的日期信息,在文件路径不变的情况下复用上一次获取的内容
js
const cache = new Map()
const cached = cache.get(url)
if (cached) {
return cached
}
并发执行异步操作
如果是 await new promise
在执行的时候才创建和获取 promise 结果,提升不是特别明显。
比如 spawn
创建子进程调用,配合 await promise
,在文章数量较多时,依然会有明显的耗时。
所以可以将文件内容和 git 时间的获取动作提前且并发执行。
js
const contentPromises = files.reduce((prev, f) => {
prev[f] = {
contentPromise: fs.promises.readFile(f, 'utf-8'),
datePromise: getFileBirthTime(f)
}
return prev
}, {})
但在测试的时候发现这样写偶尔会执行出错或提升不明显,大概是并发的执行的 Promise 和 spawn 创建子进程过多的关系。
于是引入 p-limit 来控制并发的 promise 数。
js
import os from 'node:os'
import pLimit from 'p-limit'
const limit = pLimit(+(process.env.P_LIMT_MAX || os.cpus().length))
const metaPromise = limit(() => getArticleMeta())
这里默认值使用os.cpus().length
来获取 CPU 核心的数量,这样创建的子进程能充分利用上多核的能力,不然并行值调得再大,也不会有明显的提升。
非必要第三方能力提供开关
有些能力,可能没有用到,但是打开就是会增加额外的耗时,对文件做了不改变内容的分析与处理。
在测试中发现 RSS 生成 HTML 的逻辑非常耗时,文件内容越多,耗时越多。
js
const fileContent = fs.readFileSync(file, 'utf-8')
const { createMarkdownRenderer } = await import('vitepress')
const mdRender = await createMarkdownRenderer()
const html = mdRender.render(fileContent)
vitepress
内置使用的 markdown-it
,并且内置了许多的插件,html 作为 RSS 内容的组成也不是必要的部分,因此可以做成可选的能力,交由用户选择是否开启,同时将生成的方式也做成可配置的,用户可以传入更加精简的生成方法。
另一个是 markdown 图表的渲染,主题内置的 mermaid 相关插件,发现打开即使页面里没有使用,也会增加额外的耗时,且会增加非常的多。
因此将这个也弄成默认关闭,由用户自己选择是否开启,深度优化需要修改对应插件的源码,还没来得及研究这个计划后续再做。
最后
个人觉得代码应该还有优化空间,下来再探索一下,攒一波有重大突破再来分享分享。
博客本身的优化,之前也发文章分享过来,感兴趣的可以看看:博客性能优化笔记
没错:已经拉满了!