效能字型前端

網站載入很慢?中文字型很可能是元凶

打開網站,版面空白三秒、五秒,文字才一次浮出來。這種狀況在中文網站出現的機率特別高,原因通常落在字型。我自己的網站也踩過同一個坑:從 Google 的字型服務載入五套字型,其中兩套是中文字型,畫面第一次出現文字要等 14 秒。後來改成把字型放到自己主機、並只保留站上用得到的字,這個時間降到 1.9 秒。這篇把整套判斷與處理流程寫下來,包含它的代價與長期維護成本。

1. 中文字型為什麼特別重

拉丁字型一個字重通常收 200 到 400 個字符,壓成 woff2 之後大約 20 到 40 KB。中文常用字就有五六千個,全字集收到兩萬以上,同樣壓縮完一個字重動輒 3 到 8 MB。可變字型把多個字重塞進同一個檔案,體積再往上疊。一個網站放兩套中文字型,光字型就可能超過站上其他所有資源的總和。

自我檢查

  • 一個中文全字集字重的 woff2 常見落在 3 到 8 MB,拉丁字型同條件約 20 到 40 KB。
  • 中文網站常見「標題一套、內文一套」的配置,等於同時載入兩份大檔。
  • Google Fonts 會把中文字型切成上百個 unicode-range 分片,看似分散,實際下載量仍隨頁面用到的字散佈而上升。
  • 字型是會擋住文字繪製的資源,它慢下來會直接反映在 FCP 與 LCP 上。
  • 行動網路與較舊的手機受影響最深,桌機寬頻常常把問題藏起來。

2. 怎麼確認慢的原因就是字型

最省事的方式是把網址丟進 Google 的 PageSpeed Insights,看它有沒有把字型列進「拖慢載入」的項目,順便記下改動前的分數當基準。想再確認一點,可以在瀏覽器按 F12 打開「網路」頁籤重新整理,看每個字型檔多大、下載花多久。如果畫面文字浮出來的時間,跟最後一個字型檔下載完成的時間貼在一起,元凶大致就確定了。

自我檢查

  • 網路頁籤停用快取後重新整理,篩選 Font,記下總傳輸量與最慢的那一筆。
  • 看網站有沒有跑去 Google 的字型服務(fonts.googleapis.com、fonts.gstatic.com)拿字型。每多連一個外部網站,就多一段建立連線的等待。
  • 先用 PageSpeed Insights 記下改動前的效能分數與載入秒數,之後才有得比。
  • 把行動裝置節流打開(4G、CPU 降速)再測一次,數字才貼近真實使用者。
  • 在 Performance 面板看 LCP 元素是哪一個,若它是一段標題文字,字型的嫌疑就很高。

3. 四種解法,由易到難

這四步可以照順序做,前兩個當天就會有效果,後兩個需要一點工。我自己的站四個都做了,最後五個家族合計壓到 156 KB。每做完一步就重新量一次,才知道哪一步真的有效。

自我檢查

  • 減少家族數量:盤點站上實際用到幾套字,把只出現在一兩個區塊的裝飾性字型砍掉。
  • 內文改用系統字型堆疊:中文內文交給讀者裝置上既有的字型,下載量歸零。我移除 Noto Sans TC 這一步省下 234 KB。
  • 自架字型:把字型檔放進自己的主機,省掉連往外部服務的等待,也能自己決定讓瀏覽器記住多久。
  • 子集化:只保留頁面實際用到的字符並限制字重軸。我的標題用字 Noto Serif TC 子集化後是 93.7 KB,字重軸限制在 300 到 400。
  • 系統字型堆疊寫法可參考:-apple-system、PingFang TC、Microsoft JhengHei、Noto Sans TC、sans-serif。

4. 只留用得到的字,代價在哪

工具面沒什麼玄機,有現成程式能掃出站上實際用到哪些字,產出精簡版字型檔,跑幾秒就結束。真正的成本在流程:精簡版只包含產生當下站上有的字,之後寫的新文章只要帶進一個沒收錄的字,那個字就會掉回系統預設字體,跟旁邊的字長得不一樣。

同一行文字裡混著子集字型與系統字型,粗細與筆畫風格對不上,讀起來會覺得哪裡怪。標題特別明顯,因為字大。決定採用子集化之前,要先確定自己有辦法維持重新產生的流程,否則過幾個月版面會慢慢花掉。

自我檢查

  • 產生時只指定站上用得到的字,並輸出成 woff2 這種壓縮率較好的格式;如果用的是可調粗細的字型,再把粗細範圍限縮到實際會用到的區間。
  • 字符來源建議直接從編譯後的頁面內容掃出來,比手動維護一份字表可靠。
  • 把「重新產生字型子集」接進建置流程或發文流程,新內容上線前自動跑一次。
  • 無法自動化的話,至少在標題字型的子集裡多收一份常用字表當緩衝,代價是檔案變大。
  • 把原始的完整字型檔與產生步驟留著,換人接手才重做得出來。
  • 上線前用實際文章內容目視檢查一遍標題與導覽列,漏字通常一眼就看得到。

5. 字還沒載完的那幾秒,畫面要顯示什麼

font-display 決定字型還沒下載完的期間,瀏覽器要怎麼處理那些文字。swap 會先用系統字型畫出來、字型到位再換掉,好處是文字不會消失,代價是換字瞬間的版面跳動。optional 讓瀏覽器在字型沒能即時到位時直接放棄這一次載入,版面最穩,代價是有些使用者第一次進站看到的是系統字型。

另一個設定是讓字型「插隊」提早下載,不必排在後面等。但插隊的檔案太多等於大家一起搶頻寬,反而把真正該優先的東西擠到後面。我只讓第一眼會看到的標題字型插隊,其餘照一般順序載入。

自我檢查

  • 內文字型用 font-display: swap,避免文字在下載期間整段空白。
  • 想完全消除版面跳動可考慮 optional,前提是接受部分使用者第一次看到系統字型。
  • 只 preload 首屏確定會用到的字型檔,通常一到兩個就夠。
  • preload 的 crossorigin 屬性必須跟實際請求一致,設錯會讓同一個檔案下載兩次。
  • 用 size-adjust、ascent-override 調整備用字型的度量,可以把換字時的位移縮到很小。

6. 自架字型順手解掉的幾件事

效能之外,把字型收回自己家還順手解掉兩件事。第一是安全設定變單純:網站可以規定只准跟哪些外部網站要東西,少兩個來源,這份名單就短一截,也比較好維護。第二是隱私:不必再向訪客揭露「你的連線資訊會被送到 Google 的字型服務」,德國那件 Google Fonts 判決之後,有歐盟訪客的網站對這點會比較在意。

自我檢查

  • 字型檔設長效快取(Cache-Control: max-age=31536000, immutable),檔名帶版本雜湊,改版時換檔名即可。
  • 少連兩個外部網站,就少兩段建立連線的等待,行動網路上的差距比桌機明顯。
  • 隱私權政策不必再列 Google Fonts 這一項第三方資料處理。
  • 自架前確認授權允許:Noto 與思源系列採 SIL Open Font License,自架與子集化都在允許範圍內;商用字型要逐一看合約。

7. 排除字型之後,還可能是什麼

字型處理完分數還是難看的話,接著看兩個地方。第一是圖片:沒壓縮的 JPEG、直接上傳的手機拍攝原圖、沒設寬高造成的版面位移,這些常常本身就是 LCP 元素。第二是網頁程式:程式一次做太多事的時候,畫面就算畫出來了也點不動,使用者會覺得卡。

自我檢查

  • 圖片改用 WebP 或 AVIF,依版面實際尺寸輸出,並加上 width/height 或 aspect-ratio 避免位移。
  • 首屏以外的圖片加 loading 為 lazy,首屏那一張反而要拿掉 lazy 並考慮 preload。
  • 檢查有沒有整包載入的 JS 套件、輪播與動畫函式庫,能拆的拆、能延後的延後。
  • WordPress 站先看外掛數量與佈景主題自帶的資源,很多站光停用兩三個外掛就有感。
  • 每次只改一件事並重新量測。效能分數本來就會隨測試環境、網路狀況與當下伺服器負載浮動,一次改五件會分不清誰有效。

字型是少數改動範圍小、效果卻能直接反映在數字上的項目。我的站從「第一次出現文字」14.0 秒、「主要內容顯示完成」14.5 秒、效能 55 分,改到 1.9 秒、2.8 秒與 93 分,動到的只有字型載入方式。這些數字在不同測試環境會有落差,所以改動前後要用同一套條件對照,看趨勢比看單次分數可靠。

常見問題

網站用 Google Fonts 載入中文字型會不會拖慢速度?
中文字型檔本身就大,再加上要多連兩個外部網站拿檔案,在行動網路上差距很明顯。我自己的站原本從 Google 字型服務載五套字型,畫面第一次出現文字要等 14 秒,改成自己主機提供、只留用得到的字之後降到 1.9 秒。如果站上只用一套拉丁字型,影響就小很多,不必急著改。
中文字型子集化之後,新文章出現沒收錄的字會怎樣?
那一個字會退回系統字型顯示。單獨看不一定馬上發現,放在同一行標題裡就看得出粗細與筆畫風格對不上。解法是把重新產生子集接進發文或建置流程,或在子集裡多收一份常用字表當緩衝,代價是檔案變大。沒有維護流程的話,建議先做「減少家族」與「內文用系統字型」這兩步就好。
內文中文改用系統字型堆疊,各裝置看起來會差很多嗎?
會有差異。macOS 與 iOS 走蘋方、Windows 走微軟正黑或本機版 Noto Sans TC、Android 走思源黑體,字面寬度與行高不完全一致。內文尺寸下多數讀者不會特別注意,換來的是中文字型下載量歸零。若品牌對內文字型有嚴格要求,就得留著自架字型並接受它的體積。

延伸閱讀