我想给博客换字体,最初是觉得页面太严肃,希望它能有一点个人风格。但换完之后,最明显的一次反馈是:技术文章的标题变得难以接受了。
在 Google Fonts 上看马善政毛笔楷书时,预览是一句纯中文。放进博客,标题里却有 VS Code Remote SSH、数字和单位,旁边还挤着日期与标签。那次修改又同时放大了标题字号,实际页面与预览已经不是一回事。
最后博客用了 MiSans 作为正文,Cascadia Mono 作为代码字体。比起推荐这两款字体,我更想记下这次怎么确认字体适不适合、有没有真正生效。
用真实文章试,先不要同时改字号
马善政的那次尝试中,字体和排版范围一起变了:不只首页名称,文章标题、页面标题和引用块也换成了手写体,文章标题还从原来的字号放大。
因此不能把不适感全部归因于字体。纯中文预览没有覆盖中英文混排,同时改变字号又放大了差异。后来先恢复文章页的原字号和原字体,把手写体收回到首页,才得到一个比较清楚的对照。
下一次试字体,我会固定字号、字重和行高,使用同一篇真实文章。至少要有一段长中文、一个包含英文和数字的标题,以及代码块里的中文注释。还要缩窄页面看换行,因为单行预览很容易掩盖问题。
这也解释了为什么字体网站的样张只能用于初筛。它展示的是字体在选定文本上的效果,不包含博客本身的内容密度和界面元素。
看不出变化时,先确认浏览器用了什么
接着换阿里妈妈方圆体,最初只应用到部分大标题。我看不出明显差别,于是想干脆全站统一,解决不同系统各用一套回退字体的问题。
这时 DevTools 出现了:
Failed to decode downloaded font
OTS parsing error: Unable to instantiate font face from font data请求地址以 .woff2 结尾,Next.js 也生成了静态资源 URL,但浏览器没能接受字体数据。这时继续调字重和圆角轴,无法解决字体加载失败的问题。
那轮排查后来替换了字体文件,并在 Chromium 中确认 OTS 错误消失。能确定的是原文件没有通过浏览器解析。仅凭这两行错误,不能认定下载到的一定是 HTML、压缩包或 Git LFS 指针;它们可以作为排查方向,不能写成已经查明的原因。
对 WOFF2 文件,先读前四个字节是一个便宜的初筛。下面的 PowerShell 示例读取当前目录中的字体:
$fontPath = (Resolve-Path -LiteralPath './CascadiaMono.woff2').Path
$stream = [System.IO.File]::OpenRead($fontPath)
try {
$header = [byte[]]::new(4)
$count = $stream.Read($header, 0, 4)
if ($count -ne 4) { throw '文件不足四字节' }
[System.Text.Encoding]::ASCII.GetString($header)
} finally {
$stream.Dispose()
}WOFF2 规范规定的签名是 wOF2。签名正确只证明开头符合格式,不代表内部字形表完整,更不能替代浏览器的解析检查。
浏览器里还要分开看两件事:Network 证明文件请求到了,Rendered Fonts 证明选中的文字实际用了它。Computed 中出现期望的 font-family,仍然可能只是在显示 CSS 声明,某些字符已经回退到了别的字体。
得意黑的倾斜不是 CSS 错误
方圆体之后又试了得意黑。全站换上去以后,我觉得正文一直斜着,看起来有些奇怪。
得意黑官方项目提供的字形本身就带倾斜,字体文件也使用 Oblique 命名。font-style: normal 不能把字体轮廓改成直立。那次需要换的是字形选择,而不是修一个没有写错的 CSS 属性。
对这个博客,我希望正文、导航和文章标题可以使用同一套直立字体,因此改成了 MiSans。这里没有必要评价得意黑是否“好用”:它放在展示文字中的效果,与整篇技术长文的阅读要求不同。
MiSans 负责正文,Cascadia Mono 负责代码
MiSans 最终只接入两个官方 WOFF2 字重。下面是根布局中的字体定义:
import localFont from 'next/font/local';
const miSans = localFont({
src: [
{ path: './fonts/MiSans-Regular.woff2', weight: '400' },
{ path: './fonts/MiSans-Bold.woff2', weight: '700' },
],
variable: '--font-mi-sans',
display: 'swap',
});
const cascadiaMono = localFont({
src: './fonts/CascadiaMono.woff2',
variable: '--font-cascadia-mono',
weight: '200 700',
style: 'normal',
display: 'swap',
});再把两个 variable 类名放到根元素上,让正文和代码分别引用。Tailwind 的主题配置中,对应关系是:
@theme inline {
--font-sans: var(--font-mi-sans), system-ui, sans-serif;
--font-mono: var(--font-cascadia-mono), var(--font-mi-sans), ui-monospace, monospace;
}这样代码中的拉丁字符使用 Cascadia Mono,不在其覆盖范围内的中文注释可以回退到 MiSans。微软 Cascadia 项目区分了带编程连字的 Code 和不带编程连字的 Mono。博客选 Mono,是希望示例里的操作符保持分开的显示形式;连字本身不会改变复制出来的源码字符。
Next.js 字体文档说明了 next/font/local 的注册和自托管方式。配置好以后,这轮 Chromium 检查确认正文为直立字形、标题使用 700 字重,控制台没有 OTS 或解码警告。
只提供 400 和 700 两个文件,也意味着没有真实的 500、600、900 文件可选。页面上的中间字重会进行字体匹配,不能把 CSS 写了几个数值当成已经加载了几种字重。
统一字体有流量代价
当前用于网页文字的文件大小是:
MiSans-Regular.woff2 4,858,624 字节
MiSans-Bold.woff2 5,081,104 字节
CascadiaMono.woff2 210,484 字节两份 MiSans 合计 9,939,728 字节,约 9.48 MiB。Cascadia Mono 约 206 KiB。这里只列网页文字字体,不包含分享图片生成时使用的字体资源。
这些是磁盘文件大小,不是每次访问都要支付的流量。浏览器是否请求、是否命中缓存以及预加载如何配置,需要看 Network。但第一次需要下载两种中文字体时,这个体积确实存在,接入 next/font 不会自动把完整中文字体变成几百 KB。
这次网页文字仍使用完整字体文件。后续如果要做子集,先核对字体许可允许的处理方式,再决定覆盖字符与缺字回退;下载和嵌入条款以MiSans 官方页面为准。
到这里,字体选择才算完成了一个可检查的版本:页面效果能接受,浏览器确实命中目标字体,文件也能正常解析。是否值得继续优化 9.48 MiB 的首访成本,还要结合实际访问数据。下一次再换字体,我会先在真实文章页做这个对照,不会只看预览卡片。