站點優化日誌(2022.08.12)

語言: CN / TW / HK

記錄自上次更新以來的一些變化。

主要調整內容

  • 持續完善構建工具,目前所有構建過程均基於 Golang 1.19 ,包含在上次網站更新後,新增的用於動態計算網站資料、生成分析頁面的工具。
  • 因為 Google Analytics 運營策略的原因,使用 “Google Analytics 4” 替換了 “Universal Analytics(UA)”。
  • 對 Hugo 進行了多次版本升級,當前使用的版本是 hugo v0.101.0 ,進一步微調模版,讓構建效率更高。

記錄一次瑣碎的 CI 功能重構

這個小節的內容,主要來自半年前的折騰筆記,有群裡的夥伴好奇細節,權當拋磚引玉,簡單分享下。

《站點優化日誌(2021.11.29)》 一文中,我提到新增了一個“統計分析”的 CI 階段,用於在每次釋出文章時,將文章相關的釋出資料、中英文字數進行統計分析,然後輸出為可讀性好的“報告”。

為了快速完成功能原型,程式的第一版採用了 Node.js 編寫。程式會對網站裡的一千多篇文章進行分析,整個過程需要花費十幾秒鐘。而除此之外的所有流程載入一起也不過十幾秒種,即使構建階段有一部分可以並行執行,也得額外新增十秒鐘的額外成本。

然而,在那篇文章釋出之後的一天,一個忍不住,我就把這個 CI 由 Node 替換成了 Go 實現,足足省了 12 秒(500% 效能提升)。

為了減少整體序列任務在 Docker 容器建立喚醒過程所花費的時間,還將 “convert” 和 “archive” 合併成了一個任務,又立省 3 秒。

當然,如果是平時寫完內容,進行正式預覽,所需要的時間也會更短一些。

在重構之前,需要先了解這個統計階段做了哪些事兒。

如何使用 Node 實現一個最簡單的原型

藉助 echarts,“統計頁面”的前端效果實現非常簡單,這裡就不展開了。我們著重來聊聊如何使用 Node 來快速完成一個功能原型。

想實現這個頁面的功能,最核心的兩件事是:

  1. 解析所有文章的內容,將文章內容中的 Markdown 標記語法轉換為純文字內容,並去掉裡面包含的英文內容,比如程式碼塊。然後使用最少開發成本的方式進行字元統計。
  2. 解析所有的包含文章 Meta 資訊的 JSON 檔案,將資料按照不同的維度進行篩選和分組,比如年、月、日、星期、小時,或者按照一些維度進行組合,輸出簡單的報表。

將上面的“需求”細化,我們本著 使用盡可能少的程式碼來解決問題 的思路,儘可能的使用“現成”方案。比如,可以通過使用 @anydown/maildown 來將 Markdown 格式的內容轉換為純文字。

const maildown = require("@anydown/maildown");

module.exports = function (content) {
    return maildown(content, { lineLength: 70 });
}

比如,可以使用 vscode-wordcount-cjk 專案中的中文統計方法,來完成中文字數的統計功能。

/**
 * Justify if a char is Chinese character.
 *
 * 4E00-9FFF: CJK Unified Ideographs
 * F900-FAFF: CJK Compatibility Ideographs
 *
 * Reference:
 * http://houfeng0923.iteye.com/blog/1035321 (Chinese)
 * https://en.wikipedia.org/wiki/CJK_Unified_Ideographs
 * https://en.wikipedia.org/wiki/CJK_Compatibility_Ideographs
 *
 * @todo Refine to contains only Chinese Chars.
 *
 * @param ch Char to be tested.
 */
function _countChineseChar(ch) {
    // Count chinese Chars
    const regexChineseChar = /[\u4E00-\u9FA5\uF900-\uFA2D]/;
    if (regexChineseChar.test(ch)) {
        _nChineseChars += 1;
    }
}

雖然,Node 生態裡,沒有直接能夠統計 英文片語 的軟體包、或者既簡單又具備高效能的方案。但是,我們可以使用 spawn 呼叫 wildcat 這類能夠滿足需求的、預編譯好的二進位制工具來搞定需求。

spawnSync("wildcat", ["-w", "./concat.txt"])

剩下的就是一些膠水程式碼,比如小技巧減少 IO、等等。完整的程式碼大概百十來行就能夠解決戰鬥了。

使用 Golang 來改善執行效率

Node 在快速完成原型方面和 Python 有一拼,但是想要實現高效能程式,就需要額外付出不少代價了。而且,由於 Node 生態的快速演進,NPM 軟體包、NPM 客戶端的版本都容易“過時”,過個三年五載的程式,經常出現“可復現”存在問題的情況。

雖然將應用“打成 Docker” 能夠解決很大一部分問題,但是映象體積和再次開發的體驗,都屬於一言難盡的話題。所以,當針對簡單功能考慮進行重構的時候,Go 是一個不錯的選擇:效能下限高、執行和構建結果都足夠穩定,容器映象也足夠小巧。

如何將 JavaScript 使用 Golang 進行重寫,我們就不展開了,單單提一個省事的小技巧。在前文中,我們提到了使用 Node 呼叫 wildcat 來統計片語數量,雖然在 Go 中,我們也可以使用類似的方式,使用程式外部執行呼叫的方式,來完成功能。但是跨程式呼叫,畢竟是一個很慢的操作。

本著最小化改動程式,少寫程式碼的原則,我們可以通過將 wildcat 引入程式,然後將待處理的文字內容存到臨時檔案中,然後呼叫 wildcat 的處理函式,獲得處理結果。如果追求絕對的效能和速度,還可以使用“記憶體 FS”來解決問題,比如這個專案( soulteary/memfs )。

package wordcounter

import (
    ...
	"github.com/tamada/wildcat"
)

func count(fileName string) string {
	...
}

func WordCounter(content string) string {
	tmpFile, _ := ioutil.TempFile("", fmt.Sprintf("%s-", filepath.Base("wc")))
	defer tmpFile.Close()
	tmpFile.WriteString(content)
	wc := count(tmpFile.Name())
	defer os.Remove(tmpFile.Name())
	return wc
}

當然,如果你追求更高效的處理方案,可以通過“mock stdin 和 stdout”來模擬 wildcat 使用過程中類似 “cat” 讀取 Linux Pipeline 中的資料,而無需檔案落盤。

關於網站統計的一些事情

在折騰一個臨時專案的時候,同事提醒我 GA 需要升級新版本,在閱讀了 運營條款 後,雖然服務停止時間是 2023 的 7 月 1 日,但是本著避免後面忘記升級,而導致統計資料缺失,我還是將專案使用的統計模式進行了升級調整,並更新了統計程式碼。

這裡有一個小細節,在升級統計賬號之後,別忘記進行新舊賬號的關聯,可以同時在新、老控制檯看板中觀察資料。(畢竟,新版本控制檯功能還不是很完善)

最後,在最近的一次業務測試中,挑選了能夠帶來大量讀者(十萬+)的幾個場景中,經過相對嚴謹的資料比對,發現國內使用者在 Google Analytics 中的資料流失率在 70% 以上。那麼使用國產統計平臺,是否會有質變呢?在以往的總結中,我有提到過,針對開發者群體,在相同的統計目標(網站)上,百度的資料量會比谷歌還少。

所以,如果想要更精準的得到使用者行為,進而通過相對精準群體使用者畫像,來不斷優化產品,或許除了使用三方平臺,“自建資料統計”也是避不開的事情。

如果讓我在 2022 年下半年的此時此刻,再次重新設計美團的統計 SDK。我想,我應該能夠拿出一套更完善,對賬率更高的方案 :D

Hugo 升級

作為 Hugo 的老使用者,我一直以為 0.90+ 會是 1.0 之前的最後篇章,在使用了 Hugo 幾年後,應該是能夠用上 Hugo 1.0 大版本的。

於是在上一次的網站優化日誌之後,我積極的更新了三個 0.90+ 的版本。然而意想不到的是,Hugo 居然打出了 0.100+ 的版本號。

吐槽版本帝這個事情之外,分享下我是如何調優 Hugo 的吧。在 Hugo 的老版本中,我們可以通過下面兩個命令來進行分析,找出模版中不靠譜的實現,然後進行逐個擊破:

hugo benchmark
hugo --stepAnalysis

在較新版本的 Hugo 中,我們可以通過下面兩個替換命令,來實現類似的事情:

hugo --debug
hugo --templateMetrics

在最近的版本中,Hugo 依然持續在針對資源管理上下功夫,除了之前的能夠通過簡單的內建功能(基於 Go), 來解決傳統前端依賴複雜構建工具進行開發的問題之外,還能夠針對媒體資源進行優化等。

但是,這裡的最佳實踐依舊是進行前後端分離,讓 Hugo 只做頁面生成、路由管理,而非連帶“前端”一鍋端,因為 99% 的場景下,我們根本無需對前端程式進行改變,想要獲得最好的前端程式效能,單靠“開箱即用”的非專業工具完成構建和優化,也是不太現實的。

最後

先寫到這裡了,依舊期待下一次的站點升級。期待在下一次的更新中,可以分享一個折騰驗證了許久的“實用”站內搜尋引擎方案。

–EOF