字体原理笔记
上一篇《博客字体的选择》记的是换字体的结果,现在继续说一下背后的原理。
三大字体族
-
衬线体(serif):笔画末端带小装饰角。拉丁的 Times、Georgia;中文对应宋体/明体——横细竖粗、横画末端有顿角。装饰不是审美添头,是工艺化石:拉丁衬线来自古罗马石刻收刀,宋体顿角来自雕版刀口与毛笔顿笔。气质天然联想「纸、书、正式」。
-
黑体(sans-serif):去掉一切装饰,笔画等粗。拉丁的 Helvetica、Inter,中文的微软雅黑、苹方、思源黑体。工业时代产物,气质「现代、中性、屏幕」,是当代互联网的默认审美。
-
等宽体(monospace):每字符占宽相同(Consolas、JetBrains Mono),为代码对齐而生。中文单字在任何字体里本来就是等宽方块,「中文等宽字体」的真正含义是拉丁字符恰好半个汉字宽(2:1),中西混排的代码列才对得齐——代表是更纱黑体(Sarasa Mono SC)、思源等宽。
屏幕上的衬线兴衰
低分辨率时代衬线细横渲染不出来,黑体统治屏幕。
Windows「宋体发虚」的根源即中易宋体是为小字号点阵优化的,大字号经不起 ClearType。
高分屏普及后衬线才在屏幕翻身——Noto Serif SC 就是绕开中易宋体的历史包袱。
两个细节:
-
真斜体 vs 假斜体:Georgia 的 italic 是单独设计的字形;中文无斜体传统,浏览器只能机械倾斜(假斜体)。「斜体日期」只有拉丁字符才好看。
-
老式数字:Georgia 的数字高低错落如小写字母,混入正文不打眼——它比 Noto 拉丁部分「有温度」的原因之一。
font-family
font-family: Georgia, "Noto Serif SC", SimSun
font-family 栈是「逐字」路由表,不是「三选一」,而是每个字符独立地从左到右查询,第一个拥有该字形的字体中标:
"Docker 容器" 的渲染:
D-o-c-k-e-r → Georgia 有 ✓ → 用 Georgia
容-器 → Georgia 无汉字 ✗ → Noto Serif SC ✓ → 用 Noto
推论:
-
同一行中英文分属两个字体是机制而非问题。
-
栈序即设计决策:Georgia 在前,英文数字全走 Georgia;若中文字体在前,它自带的拉丁字形会拦截英文,Georgia 永远轮不上(本站踩过:一行之差决定味道)。
-
栈的末尾永远放系统兜底(SimSun/serif),保证任何字符都有着落。
子集化
字量问题:拉丁字体几百个字形、几十 KB,西方网站随手全量引入 webfont;中文字体两万多字形,一个字重完整文件 8–12MB,没法上网页。
子集化(subsetting):把字体裁到「本站实际用到的字符」。博客约 1500 个唯一字符,裁后一个字重约 400KB。
代价:子集是快照。文件里只有裁剪那一刻存在的字。新文章用了子集外的字(如「吝啬」)时:吝 → Noto 子集无 ✗ → 沿栈下落 → SimSun ✓ → 仅这个字用宋体渲染
自定义字体构建
1. 构建时比对:每次构建把 dist 产物的全部字符与基线 charset.txt 对比,缺字警告。
have = set(open("static/fonts/charset.txt", encoding="utf-8").read())
chars = set()
for dp, _, fs in os.walk("dist"): # 以产物为准:
for f in fs: # 已发布内容+UI 文案,
if f.endswith((".html", ".js", ".css")): # 草稿不计入
chars.update(open(os.path.join(dp, f), encoding="utf-8").read())
miss = {c for c in chars - have if c.isprintable()}
if miss:
print(f"字体子集缺 {len(miss)} 个字符(这些字将回退宋体): {''.join(sorted(miss))}")
2. 重切:先重新提取字符集写回基线,再用 fonttools 从源字体切出新子集:
python3 -m fontTools.subset /tmp/NotoSerifSC-Regular.otf \
--text-file=static/fonts/charset.txt \
--flavor=woff2 \
--layout-features='*' \
--output-file=static/fonts/NotoSerifSC-400.woff2
参数:
--text-file留哪些字--flavor出什么格式(woff2,brotli 压缩)--layout-features='*'保留全部 OpenType 特性,标点挤压和 kern 都在里面,省那几十 KB 不值得
3. 缓存级联失效:字体 URL 的 ?v=哈希 写在 style.css 内,css 自身也带哈希——字体变 → css 哈希变 → 页面引用变,任何缓存层都不会卡旧字体。