在進行網頁設計時,有時為了提升品牌質感,設計師們會希望使用一些非系統預設的字型,常見的有 Google 與 Adobe 合作的 Noto Serif TC (思源宋體) 等 Webfont。但一個中文字型+字重動輒 15MB~20MB 的檔案大小,對前端來說會是不小的負載。
為什麼中文字體是網頁效能的殺手?
根本的原因在於英文字體僅需包含約 200 個字元,檔案通常在 50KB 以下;而中文由於一字一意的特性,為了涵蓋常用字與罕用字,動輒高達 20,000 個以上的向量資料,即便只算常用字,也需要覆蓋 8000-12000 左右的字彙量。每加一種字重,檔案又會乘倍跳,用得越多,負載越重。所以當我們在使用 Webfont 時,就需要考慮中文方塊字的特性,在視覺效果與負載之間找到平衡。
子集化 (Subsetting)
針對這個問題的解決方式是:子集化(Subsetting)。你可以將它理解成把大包裹拆分成小包裹,這樣搬運起來就不會那麼吃力,常用的方式有下面幾種:
一、使用線上工具自己打包 (適合少量文字)
如果你只需要處理幾個固定的字,如:公司名稱。這時可以使用 Font Squirrel Webfont Generator 網頁工具,將原本的字型檔拆分出你需要使用的部分。方式如下:
- 上傳 NotoSerifTC-Regular.otf。
- 在 Expert 模式下的 Subsetting 選項中,選擇「Custom Subsetting」。
- 在文字框輸入你要保留的字 (例如:歡迎光臨歐森沃克)。
- 下載產出的 .woff2 檔案。
- 優點:簡單、檔案小。
- 缺點:流程複雜,網頁內容只要變動過,整個流程就需要再重新打包一次。
二、Google Fonts API:最聰明 (適合少量的動態內容)
Google Fonts 內建了子集化參數,你不需要自己切檔案。如果你知道頁面上會出現哪些字,可以直接在 URL 後面加上 &text= 參數。適合使用在內容會動態改變的位置,如標題、Slogan 等,方式如下:
<link href="https://fonts.googleapis.com/css2?family=Noto+Serif+TC&text=歐森沃克" rel="stylesheet">
- 優點:極速加載,檔案大小通常不到 5KB。
- 缺點:只有在 URL 中指定的字會套用該字體,其他字會跳回系統預設字,且受限於 URL 長度的限制。
三、Google Fonts 的自動優化解決方案
如果你是透過 Google Fonts 引入 Noto Serif TC,Google 會自動執行「切片技術」(Unicode Range Splitting)。它會將中文字拆分成數十個較小的 woff2 檔案(每個約 20KB~100KB)。網頁只要下載使用到的切片包即可,而不是下載整個字型檔。
- 優點:簡單且免費。
- 缺點:一些對 Google 不友善的國家會無法使用。
另外,除了 Google Fonts 提供的免費解決方案之外,如果你們公司也有購買其他的商用雲字型的服務,如:justfont、文鼎等,也有提供類似的動態載入技術。
四、前端自動化:Fontmin (Node.js)
如果你希望在編譯前端專案時自動掃描 HTML 裡的文字並切字體,可以使用 Fontmin。下面是一個簡單的範例:
const Fontmin = require('fontmin');
const fontmin = new Fontmin()
.src('src/fonts/NotoSerifTC-Regular.otf')
.use(Fontmin.glyph({
text: '這是一些需要子集化的文字'
}))
.use(Fontmin.ttf2woff2()) // 轉成 web 專用的 woff2
.dest('build/fonts');
fontmin.run();
- 優點:彈性大、覆蓋範圍廣,可以動態產生字型檔。
- 缺點:需要有一定的技術背景,視覺設計師比較難上手。
五、使用上需要注意什麼?
5.1 從 Local 到 FOUT 優化三步驟
在 CSS 的 @font-face 中,我們可以透過組合屬性來優化載入體驗:
- 第一步優先載入本地字型:如果預設用戶的電腦裡已經安裝思源宋體的話,可以再加上
local()屬性,它可以直接使用用戶的字型,省去下載的時間。
也許有人會問,font-family 也會找用戶端的字型,它和 local() 有什麼不同?兩者看似功能重覆,實則不然。瀏覽器在看到 font-family 後,會先去找對應的資源,當他看到 Noto Serif TC 就會去下載字型檔,等下載完成後才發現,原來用戶家裡已經安裝了,而 local() 則相反過來,它會先檢查用戶的電腦裡有沒有 Noto Serif TC,沒有才去下載對應的字型檔。
也就是說,local() 是個貼心的小棉襖,它會對瀏覽器說:「外頭天氣冷 (網路環境變化多),如果家裡 (本地) 有,就別出門 (下載) 了!」
@font-face {
font-family: 'Noto Serif TC';
/* 優先尋找本地字型名稱 */
src: local('Noto Serif TC Regular'),
local('NotoSerifTC-Regular'),
url('subset-NotoSerifTC-Regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
- 第二步使用 font-display: swap:在 CSS 的 @font-face 中加入此屬性,確保在字體下載完成前,系統會先以內建字體顯示內容,避免「隱形文字」問題。
@font-face {
font-family: 'Noto Serif TC'; src: url('subset-NotoSerifTC-Regular.woff2') format('woff2');
font-display: swap; /* 先顯示系統字,下載完再更換 */
}
- 第三步減少排版跳動 (CLS): 雖然 swap 解決了文字看不到的問題,但當 Webfont 下載完成後替換系統字時,常會因為字寬不同導致段落跳動(稱為 FOUT)。這時可以使用 size-adjust 微調「備用字體」的比例,讓它在視覺上接近 Webfont。
/* 1. 定義一個與 Noto Serif TC 比例接近的備用字體 */
@font-face {
font-family: 'Fallback-For-Noto';
src: local('Microsoft JhengHei'), local('PingFang TC'); /* 使用系統黑體當基底 */
size-adjust: 95%; /* 調整縮放比例 */
ascent-override: 90%; /* 調整上升高度 */
descent-override: 20%; /* 調整下降高度 */
}
/* 2. 套用在專案中 */
body {
font-family: 'Noto Serif TC', 'Fallback-For-Noto', serif;
}
5.2 加入預連線
如果使用 Google Fonts 的解決方式,你還可以在 head 加入 preconnect 預連線,提前跟 Google Fonts 的伺服器建立連線,讓字體下載更快。方式如下:
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
5.3 提供完善的替代方案
不論你選擇的字型有多漂亮,最後請務必加上系統常用的字體,以確保在特定的情況發生時,網頁仍可以順利顯示。
/* 標題與引言使用宋體 */
h1, h2, blockquote { font-family: "Noto Serif TC", serif; font-weight: 700; }
/* 內文使用系統字 */
body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans TC", sans-serif; }