Oops…你的網站,也許不適合 Google Fonts API
不少網站為了讓整體風格一致,會選擇介接 Google Fonts API 使用有別於系統原生的字體,而思源黑體及思源宋體是中文網站常見的選擇。
使用 Google Fonts API 載入字體相當方便,只需要在 html 檔案中加入一小段 code:
<link href="https://fonts.googleapis.com/css2?family=FAMILY_NAME:wght@WEIGHT_OR_RANGE&display=swap" rel="stylesheet">
它會回傳一份所有字體 @font-face
規則 CSS 檔,好處是我們並不用一次載入整包字體檔案,瀏覽器會根據頁面上有出現的文字與這份檔案中的 unicode-range
做比對,再去下載相對應的字體子集檔案,達到「有需要再下載」的效果。
看起來這麼美好,《天下》的數位專輯為什麼不適合?
在說原因之前,先以前端的角度來提一下我們數位專輯的特性:
- 頁面中有大量中文內容
- 文中不時穿插資料視覺化圖表
- 需要用到 5 種不同字重的 Noto Sans TC
- 專輯架構主要是 Client-Side Rendering
- 時常搭配 JS/CSS 產生特殊的轉場效果(滾動敘事、SVG 動畫)
這意謂著:
- 一開始需要載入不少東西
- 有時讀者滾動時也會觸發 JS/CSS 效果
- 字體檔案就算是接 Google Fonts API 還是很大
在幾次專輯製作中發現,明明只用了些簡單的動畫效果,我們的頁面在移動裝置上滑動時常常會有 jank 的情形發生。
什麼是 jank?按照字面解釋有垃圾、不穩定的意思。在程式上,代表處理主要任務的機器過載了,所以讓後續的任務卡住,畫面無法順利更新。例如在下面的測試機示範畫面裡,當下滑時會出現畫面空白,N 秒之後畫面才會出現 。事實上,我們在JS/CSS 在寫法上都做了不少優化,等所有東西載完再滑也還是有一樣的狀況。
這對以內容為主的數位專輯影響很大,因為讀者可能會因為 jank 而錯過某些內容,嚴重影響閱讀體驗。
問題到底出在哪裡?
一般來說出現 jank 是因為有比較耗運算資源的工作堵住了瀏覽器的 main thread ,才會影響到整個 rendering pipeline 的運作,講白話一點就是渲染畫面的流程塞車了。
但用了監測工具看了滑動過程的狀況,特別針對 JS / Style / Layout 不同階段都做了寫法上的優化,問題還是依舊存在。(優化手法可以再開一篇來討論,篇幅關係請容我先直接帶過。)
正當我摸不著頭緒、開始亂試時,發現把整個 Google Fonts API 拿掉,讓頁面直接吃系統原生字體,可以顯著改善 jank 問題!
嗯,頁面在手機上變的超級無敵順,但頁面不能沒有字體存在啊。
為什麼拔掉字體檔案可以改善那麼多?說真的到現在我還不是很清楚確切的原因(歡迎留言討論。文末也附上我摸索的答案和參考資料),但至少確定字體檔案是一個值得嘗試優化的方向。
Google Fonts API 的中文字體
前面有提到 Google Fonts API 的做法是把多達數 MB 的單一字重字體檔切分成數百個大小約在 20~50 KB 的字體子集檔案,透過 @font-face
機制載入頁面上所需要用到的的子集就好了。
這是我們專輯經常使用 Google Fonts API 所提供的 @font-face
CSS 檔案:https://fonts.googleapis.com/css?family=Noto+Sans+TC:100,300,400,500,700|Roboto:300,400,500,700&display=swap
從網址中其實就可以發現我們思源黑體就用了 5 種不同的字重,Roboto 則是用了 4 種,點開檔案後也可以知道 Google Fonts API 將上面的字體切割成 533 份字體子集。
實際載入時並不會 533 份子集都載入,只會載入頁面上所需要的字體子集。
可以發現實際需要載入的檔案其實不小,但因為已經嘗試過就算讓檔案全部載完還是無法解決 jank 問題,所以載入速度並不是這篇文章會關注的點。(檔案一定是越小越好,如果想優化整體字體載入策略,推薦 Harry Roberts 所寫的 The Fastest Google Fonts,他經常分享網頁效能的相關知識。)
如果用線上工具去檢視每份子集的檔案,可以發現以繁體中文的思源黑體中,每份子集約包含 200 多種字符,而這 200 多種字符在頁面中真的需要用到嗎?
其實不必然,所以我們可以針對字體檔案做幾個優化:
@font-face
CSS 檔案可以更小( 目前 261 KB)- 減少字體子集檔案數量(目前 86 份)
- 字體子集只包含頁面真正需要的字符,減少子集檔案大小(目前約 4 MB)
自製字體子集(font subseting):使用 pyftsubset
必須要自製字體子集才能做到前面所列舉出的優化,自製字體子集的工具有很多種,目前我們是使用 fonttools pyftsubset,它的優點是能調整的參數很多,因爲是使用 python 所撰寫的指令工具,可以進行相當高度的客製化,也方便與其他工具做整合。
使用教學相當推薦自由接案工程師 Markos Konstantopoulos 所寫的 Creating font subsets,他手把手教學如何使用 pyftsubset 的指令,也有詳細介紹字體檔案相關的背景知識,讓我們知道在產製字體子集時有哪些眉角需要注意。
中國20大專輯使用自產字體子集的相關數據:
與上一張圖對比,可以觀察到:
@font-face
CSS 檔案大小:261 KB → 3.4KB- 減少字體子集檔案數量:86 → 10
- 字體子集檔案大小:約 4 MB → 1.5MB
如果再針對字重來個別客製化子集,字體子集檔案大小估計還可以再減少 50% 以上。檔案大小是減少了很多,那最關鍵的 jank 問題有被解決嗎?有的!
如果將手機連結至電腦使用效能工具進行監測:
可以發現使用 Google Fonts API 字體下,瀏覽器對物件佈局和樣式次數都比自產字體子集多上兩倍,從工具紀錄的截圖中也可明顯觀察到因為 jank 的關係中間有一段並沒有顯示出來,進而影響 CPU 用量以及 JS 的執行。
最後再把自產字體子集的功能整合進專輯開發流程,在專輯發布前解析最終版本的文字內容,就可以輕鬆產生輕量化的字體子集。
自產字體子集讓我們得到了效能,那有捨去什麼嗎?
捨去了一點彈性。
所以這種作法對需要經常進行文字更新的專輯(像是疫情動態)較不適合。
(現階段我們的數位專輯是採文/code分離,所以如果只更新文字的話,並不一定要整包專輯重新打包,詳情可以參考我們之前寫過的這篇文章。)
Google Fonts API 幾乎能涵蓋到所有中文字符,所以比較沒有掉字的困擾,那除了重新打包以外,有其他可能的解法嗎?
自產字體子集 x Google Fonts API
就算我們用了自產字體子集,只要繼續引入 Google Fonts API 的 @font-face
CSS 檔案,還是可以透過 CSS 寫法上的優先級排序,把 Google Fonts API 當作備案使用。
當頁面上的字符在自定義的 CW-Noto-Sans-TC
子集中找不到相對應的檔案時,就可以透過 Noto Sans TC
去向 Google Fonts API 要它們的子集檔。
我的網站該自製字體子集嗎?
其實我們也並不是所有數位專輯都適合使用字體子集,就像前面所提到的,需要進行大量文字更新的專輯還是用 Google Fonts API 會方便許多,而且如果頁面對效能沒有什麼要求(像是我們的選舉網站),字體問題其實對讀者體驗其實影響不大。
在決定要自製字體子集前,覺得經營/開發網站的技術人員可以自問一些問題:
- 網站在桌機及移動裝置上體驗順利嗎?
- 網站目前有更明顯的問題還沒優化嗎?
- 網站文字內容需要經常做動態更新嗎?
- 對於眼前的效能問題已經窮途末路了嗎?🥲
不同字體來源影響效能的猜想
在文中有提到目前只能透過最終監測的結果,觀察到自產字體子集對移動裝置上頁面整體效能有極大的幫助,但為什麼會有幫助?在做了一些研究後還是沒辦法找到正確的答案,對此提供一些自己的猜想,希望大家可以一起集思廣益🤓:
- 移動裝置瀏覽器在遍歷大量
@font-face
規則時需要花不少效能。 - Google Fonts API 所提供的字體子集中,缺少一些 Layout features 讓移動裝置瀏覽器在光柵化( Rasterization )時候不順利。
- 不知明的原因與 JS/CSS 打架。
以下是小弟參考過的一些文章及文件供大家參考:
- W3C CSS Fonts Module Level 3
- Populating the page: how browsers work
- Inside look at modern web browser (part 3)
- Adventures in Text Rendering: Kerning and Glyph Atlases
以上就是我們對於使用 Google Fonts API 及自產字體子集的經驗分享,歡迎大家一起討論!