部落格構建效能最佳化筆記 | 提速 3 倍

粥里有勺糖發表於2024-06-12

筆者的部落格基於 VitePress 搭建的,使用其自定義主題能力完成部落格主題 @sugarat/theme 的搭建。

前段時間有群友反饋說使用主題構建後耗時增加非常明顯。

前後耗時大概增加了 10 倍,過於離譜了。

斷斷續續的投入差不多 1 個月的時間完成了最佳化,效果還是很明顯。

至此寫篇文章記錄&分享一下最佳化過程。

先看一下最佳化前後的效果

  • 測試專案:筆者的部落格,差不多 490 篇文章。
  • 測試機器:Mac Mini (M1, 2020)

僅開啟部落格相關的樣式能力

VitePress 預設主題 最佳化後主題 最佳化前主題
16.38s 20.56s 32.36s
對比目標 +4.18s +15.98s

開啟擴充能力

RSSpagefind 離線搜尋

最佳化後 最佳化後 最佳化前
RSS不開啟HTML生成 + 離線搜尋 RSS + 離線搜尋 RSS + 離線搜尋
25.70s 30.93s 50.85s
+9.4s +14.55s +34.47s

小結

整體提速約 3 倍:

  • 只開啟基礎能力:額外耗時從 16s 縮短至 4s
  • 擴充能力耗時:額外耗時從 34s 縮短到 10 s

問題定位

先定位耗時的位置,再想辦法進行最佳化。

我們可以直接用 console.timeconsole.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 相關外掛,發現開啟即使頁面裡沒有使用,也會增加額外的耗時,且會增加非常的多。

因此將這個也弄成預設關閉,由使用者自己選擇是否開啟,深度最佳化需要修改對應外掛的原始碼,還沒來得及研究這個計劃後續再做。

最後

個人覺得程式碼應該還有最佳化空間,下來再探索一下,攢一波有重大突破再來分享分享。

部落格本身的最佳化,之前也發文章分享過來,感興趣的可以看看:部落格效能最佳化筆記

沒錯:已經拉滿了!

相關文章