目標是在 Hugo 中使用 Shiki syntax highlighter 取代內建錯誤百出的 Chroma,並且大幅加快 Shiki 執行速度。本文內容和大部分使用 shiki 的人無關,這篇文章的用途是將已經 build 完的 HTML 檔案加上 shiki highlight。
前言
原本都想好要針對最多人用的 PaperMod 主題為例寫使用教學的,但是實際使用時發現 PaperMod 修改了 code block 的 CSS,也就是說教學內容不會永遠成立,所以就改成寫效能分析了。
回到內容,Hugo Blowfish 內建的 Chroma CSS 語法解析錯誤百出,同樣都是 指令 --參數
格式結果上下兩行 highlight 結果不一樣,除此之外主題切換要這樣用有夠麻煩,後來換成開箱即用的 PrismJS 和 HLJS,但是效能死宅的我不喜歡客戶端渲染,之後看到 eallion 的 shiki 文章 二話不說馬上換成 shiki。
換成 Shiki 前請先注意這些功能問題和使用細節:
- Code block 的語言務必正確標記,否則會完全沒顏色
- 無法使用 Hugo 內建的 line-highlight 等等的 code block 功能
- 不同 Hugo 主題 CSS 需要各自處理,本文只有 Blowfish 和 PaperMod 兩個主題作為範例
- Code copy buttom 也很有可能消失,需要自行解決
- 開發階段即時預覽的功能不適用於
hugo server --renderToMemory
不同做法
Eallion 的做法
eallion 的文章以 rehype CLI 方式完成,分成三個步驟
- 關閉 Hugo 內建的渲染
- 設定 rehype 和 shiki 的 CLI
- 以 rehype 的 CLI 幫已經建立好的 HTML 頁面加上 shiki 語法
這個方案有幾個問題,第一是速度非常慢,我的網站不到 60 個 md 文件就要六秒渲染,eallion 的網站有 600 個 md 文件耗時 191 秒;第二個是記憶體用量,這個方式會一次處理目標資料夾裡面的所有全部 HTML 文件,記憶體用量非常大;第三是無法在編輯時預覽,要開啟另外一個終端執行 shiki 指令才可以看到語法上色結果。
以 eallion 的網站作為測試,在 M1 MacBook Pro 上執行 rehype shiki CLI,耗時 191 秒。
Orta Therox 的做法
Orta 是 shiki 的 contributor,不只是 contributor,他的部落格也剛好是用 Hugo 搭建的,所以他的方式應該很有參考性:以腳本執行 shiki,無須 rehype 並且有基本的過濾機制,不過沒有進階的效能優化。
以 eallion 的網站作為測試,在 M1 MacBook Pro 上執行 Orta Therox 的 shikify.ts 耗時 5.9 秒。
我的做法
我的做法同樣使用腳本完成,並且有幾個優化
- 更複雜的過濾方式
- 可以指定哪些目錄需要修改
- 每個目錄可以指定排除的路徑,例如 Hugo 會自動生成 page 頁面,此路徑的 HTML 全數排除
- 對於大型專案,可以設定開發時只監視指定目錄避免不必要的重複處理
- 增量處理:根據觀察,由於本地開發時 Hugo 只會重新渲染和這次修改有關的檔案,所以基於 HTML 檔案的修改時間進行快取,避免不必要的重複處理
- 更高效的處理方式
- 使用非同步方式處理檔案 IO,使用多線程執行 shiki
- 根據 CPU 核心數、待處理檔案數量設定線程數
- 每個線程批量獲取任務避免通訊開銷
- 根據觀察,多數 HTML 頁面其實沒有 code block,因此使用 early return 避免 parser 解析壓根沒有 code block 的頁面
- 使用高效的 htmlparser2 而不是 jsdom/cheerio
以 eallion 的網站作為測試,優化完成後在 M1 MacBook Pro 上執行只需要 1.88 秒,並且開發時預覽修改可以在一秒內完成。
效能分析
總結三種方式,CLI 方式最簡單但是耗時最久需要 191 秒,而 Orta 只是換成簡單的腳本就可以把時間縮短到 5.9 秒,有 32 倍的效能提升;我的方式可以把時間再壓縮到 1.88 秒,速度提升高達 101 倍,而且在開發階段受惠於快取機制可避免重複處理,所以在 0.8 秒左右就可以完成 highlighting,速度提升高達 240 倍。
--- config: xyChart: width: 1000 height: 600 themeVariables: xyChart: plotColorPalette: "#00A2ED" --- xychart-beta title "測試 shiki highlight 速度差距倍率,以 eallion.com 為範例專案" x-axis ["My script (cached)", "My script (uncached)", "Orta's script", "Rehype CLI"] y-axis "Speedup Multiplier" 0 --> 250 bar [238.75, 101.6, 32.37, 1]
如果你想要在開發階段獲得極致的反應速度,腳本也可以設定 TARGETS_DEV
控制開發階段的渲染目標,將其限制為只有正在修改的檔案甚至能 0.25 秒完成渲染,比完整渲染快 764 倍。
和不同方案比較之後這裡再分析自己方案的效能,使用 CLInic.js 進行效能分析,以 pnpm clinic flame -- node scripts/shiki/index.js
測試,完整的火焰圖如下:
※ 包含 V8 引擎開銷的火焰圖
可以看到 V8 引擎佔據七成時間,這時如果把我們自己的 shiki 關閉,再來對比上下兩張圖,可以看到差距非常小,代表沒有優化空間了:
※ 只關閉 shiki 的火焰圖
不過我們還是簡單觀察一下目前的效能瓶頸,把 V8 和依賴的耗時都關掉,結果如下
※ 只剩下 node 和 shiki 的火焰圖
紅框處是我的函式耗時,可以看到有一半的時間花在掃描檔案上,因為 eallion 的構建結果有高達 6000 個資料夾 (find ./public -mindepth 1 -type d | wc -l
輸出 6451),所以耗時長是可以預測的。
唯一一個要改的地方是在 dev 階段應該讓線程和 shiki highlighter instance 持續存活,但是現在是直接呼叫腳本重新執行。
使用教學
本文的程式碼位置在這裡:
- scripts/shiki:執行 highlight
- dev-example.js:用於開發階段及時預覽
設定方式
先關閉 Hugo 的 code fences,找到你的 Hugo 設定檔
|
|
接下來安裝依賴套件,以 pnpm 為例
|
|
套件說明
- chalk: 日誌工具
- dom-serializer html-entities htmlparser2: HTML 解析
- serve: 即時預覽
- shiki: 語法上色
下一步是修改 CSS,以 Blowfish 為例,custom.css
只需要依照 shiki 官方文檔修改:
|
|
但是有些主題會修改 code block 的 CSS 導致文檔方式不適用,例如很多人用的 PaperMod 就有這個問題,所以在 PaperMod 要改成這樣:
|
|
設定完 CSS 後就可以開始 highlight 了,複製腳本到指定位置
- scripts/shiki:執行 highlight
- scripts/dev-example.js:用於開發階段及時預覽
然後進入 scripts/shiki/config.js
修改客製化選項,有幾個重點項目
- 處理的目標資料夾
TARGETS
,是hugo server
的輸出目錄 - 是否啟用雙主題
ENABLE_DUAL_THEME
- 主題選擇
THEMES
最後使用 node scripts/dev-example.js
啟用開發預覽,以 node scripts/shiki/index.js
幫建構完成的 HTML 檔案 highlight。
修復 Code Copy
Code copy 不同主題的設定方式不同,這裡以 Blowfish 主題為例。Blowfish 複製按鈕在 themes/blowfish/assets/js/code.js
設定,簡單來說就是找到所有的 highlight
class,這是 Hugo 內建的 code block 標示,然後每個都加上複製按鈕,前面設定關閉 codeFences 後這個 class 也沒了,所以要自己處理。
廢話不多說,修改程式碼,在 assets/js/code.js
貼上以下設定就完成了:
// Unable to extract content from: https://raw.githubusercontent.com/ZhenShuo2021/blog.zsl0621.cc/8fad76f43ecb73f96ca6922a8e5143fa3e98dda2/assets/js/code.js
使用須知
- 腳本的原理是找到目標 HTML 文件,看看裡面有沒有
<pre><code
需要被處理,有的話就定位裡面的<code class="language-CODE_LANGUAGE">
幫指定語言上色 - 如果你的 HTML 沒有被上色,請檢查 config.js 裡面的 TARGETS 設定,或者如上一個段落說的檢查 CSS
- 如果你的語言不在預設範圍內,請在 LANGUAGES 新增語言;如果你的語言沒辦法被正確識別,使用 LANGUAGE_ALIAS 做映射
node scripts/shiki/index.js --dev
可以將目標目錄改為TARGETS_DEV
以便開發時迅速預覽- Shiki 直接修改 HTML,所以你的檔案會變大,Hugo minify 也會不那麼 mini,但是經過測試的結果如下,一樣是 eallion.com:
|
|
看似增加很多容量,不過這是 top 10,實際上總容量只多了不到 2MB,增加了 1.3%,分析腳本在此。