筆者的部落格基於 VitePress 搭建的,使用其自定義主題能力完成部落格主題 @sugarat/theme 的搭建。
前段時間有群友反饋說使用主題構建後耗時增加非常明顯。
前後耗時大概增加了 10 倍,過於離譜了。
斷斷續續的投入差不多 1 個月的時間完成了最佳化,效果還是很明顯。
至此寫篇文章記錄&分享一下最佳化過程。
先看一下最佳化前後的效果
- 測試專案:筆者的部落格,差不多 490 篇文章。
- 測試機器:Mac Mini (M1, 2020)
僅開啟部落格相關的樣式能力
VitePress 預設主題 | 最佳化後主題 | 最佳化前主題 |
---|---|---|
16.38s | 20.56s | 32.36s |
對比目標 | +4.18s | +15.98s |
開啟擴充能力
RSS,pagefind 離線搜尋
最佳化後 | 最佳化後 | 最佳化前 |
---|---|---|
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
列印出耗時資訊。
console.time('flag')
// 執行程式碼
console.timeEnd('flag') // 列印出耗時
主要關注有迴圈和外部呼叫的邏輯,在其前後加上列印耗時。
簡單打了幾個點,就有如下的結果咯 ⏰。
在主題入口和兩個外掛都有一段類似的程式碼邏輯,讀取檔案內容構造 meta 資訊。
最佳化方式
非同步操作檔案
讀取檔案內容用於提取 frontmatter
資訊,生成描述,標題等內容,會用於首頁渲染。
使用 fs.promises
非同步操作檔案,這樣可以避免阻塞程序。
// 原
fs.readFileSync(filePath, 'utf-8')
// 新
fs.promises.readFile(filePath, 'utf-8')
非同步建立子程序
主要透過呼叫 git
指令獲取檔案最後的修改時間,用於展示文章的最後的修改時間。
原來使用的 spawnSync
,同樣也是同步執行的方法。
使用 spawn + Promise
替換 spawnSync
,避免阻塞程序。
// 原
spawnSync('git', ['log', '-1', '--pretty="%ci"', url])
// 新
const child = spawn('git', ['log', '-1', '--pretty="%ai"', url])
使用快取
在日誌裡可以發現,Vite 外掛裡 load
鉤子在 vitepress build 時執行了2次。
因此針對會重複執行的邏輯,可以新增新增一段快取讀寫的邏輯,能明顯降低二次執行相關邏輯的時間。
時間的獲取使用 Map
快取檔案的日期資訊,在檔案路徑不變的情況下複用上一次獲取的內容
const cache = new Map()
const cached = cache.get(url)
if (cached) {
return cached
}
併發執行非同步操作
如果是 await new promise
在執行的時候才建立和獲取 promise 結果,提升不是特別明顯。
比如 spawn
建立子程序呼叫,配合 await promise
,在文章數量較多時,依然會有明顯的耗時。
所以可以將檔案內容和 git 時間的獲取動作提前且併發執行。
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 數。
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 的邏輯非常耗時,檔案內容越多,耗時越多。
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 相關外掛,發現開啟即使頁面裡沒有使用,也會增加額外的耗時,且會增加非常的多。
因此將這個也弄成預設關閉,由使用者自己選擇是否開啟,深度最佳化需要修改對應外掛的原始碼,還沒來得及研究這個計劃後續再做。
最後
個人覺得程式碼應該還有最佳化空間,下來再探索一下,攢一波有重大突破再來分享分享。
部落格本身的最佳化,之前也發文章分享過來,感興趣的可以看看:部落格效能最佳化筆記
沒錯:已經拉滿了!