先交代一个我自己踩过的坑。前阵子做官网改版,设计稿里用了思源黑体,我在 macOS 上直接写了 font-family: "Source Han Sans SC", "Microsoft YaHei", sans-serif;,本地预览一切正常。结果客户在 Windows 上一打开,整段正文变得又木又笨,行高和字距都不对,标题字重甚至像被硬撑出来的。我盯着代码查了半天,样式没有任何问题,问题出在从 font-family 的解析顺序、系统字体回退策略,到渲染引擎的差异控制,这一整条链路我都没有认真设计过。
这篇文章不打算只跟你讲某几个 CSS 属性,而是从真实项目里最容易翻车的几个环节入手,把字体栈、@font-face 加载、可变字体、特殊文字效果和移动端适配串成一条完整链路。适合刚入门前端但总在字体上纠结的同学,也适合被设计师追问"为什么页面没有稿子里好看"的页面开发。读完你至少能少踩三分之二的字体坑。
1. 字体栈(font stack)为什么总是"第一眼感觉不对"
1.1 font-family 的解析不是"顺序选第一个存在的"那么简单
很多人以为 font-family 的机制是:浏览器按顺序检测系统里有没有这个字体,有就使用。现实中大多数时候确实是这样,但一旦文本内容比较复杂,比如中英文混排、包含数字、生僻字、emoji,行为就不是这个逻辑了。浏览器的字体回退其实更接近“按字形片段去匹配”——font-family: "PingFang SC", "Helvetica Neue", sans-serif; 这一段 CSS 的意思是,优先尝试让 PingFang SC 渲染所有字符,当 PingFang SC 里缺少某个字符(比如某些生僻汉字或特殊符号)时,用户代理会跳到下一个字体去找能覆盖该字形的字体,而不是一门心思死等第一个字体加载完成。
这带来一个很实际的坑:如果某段中文里夹杂了大段拉丁字母或数字,而中文字体本身包含这些字符,浏览器通常会整段使用中文字体渲染,包括英文和数字。很多设计稿里的数字风格是偏几何感的西文字体,页面上却显示成了中文全角风格或中文自带的半角字形,视觉上一个像被“喂胖”了,一个则过于紧凑。解决办法很简单,西文字体要放在中文字体前面,例如 font-family: "Inter", "PingFang SC", sans-serif;,这样中文用 PingFang,英文和数字优先落到 Inter。
css复制body {
font-family: system-ui, -apple-system, BlinkMacSystemFont,
"Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
}
上面这份是很多海外站点在用的“系统原生字体栈”,优点是零网络请求、性能好。可它有个致命问题:没有声明中文字体。在 macOS 上缺中文字体时,系统可能会回退到 Songti SC(宋体)一类衬线字体;在部分 Linux 环境里甚至可能落到非常难看的点阵字体。所以在中国大陆项目或中文内容为主的站点,底线是把中文黑体族接进去。
1.2 不同平台的中文字体长什么样,别指望统一
我把平时项目里最常用的一份“中文安全字体栈”整理一下,它至少能应付 mac、Windows 和安卓 WebView 三种主要环境:
css复制:root {
--font-sans: "Inter", "SF Pro Text",
"PingFang SC", "Hiragino Sans GB",
"Microsoft YaHei", "Noto Sans CJK SC", sans-serif;
}
你会看到这个变量里中文字体的顺序是 PingFang SC → Hiragino Sans GB → Microsoft YaHei → Noto Sans CJK SC。这背后对应的是字体的实际可用平台:PingFang SC 是 macOS / iOS 上 2015 年之后的内置黑体,Hiragino Sans GB 是 macOS 上老版本也能查到的冬青黑体;Windows 没有 PingFang,会跳到 Microsoft YaHei(微软雅黑);安卓和部分 Linux 桌面通常没有微软雅黑,最后落到各系统基于思源黑体/Noto Sans CJK 做的默认无衬线中文字体上。
这里要单独提醒:直接写一个 Microsoft YaHei 去覆盖所有平台是行不通的。macOS 没有微软雅黑,浏览器会继续查找后面的字体,如果后面什么都没写,系统回退的结果很可能是“宋体风格”的中文衬线字体,视觉上就会显得复古而不协调。另外,很多国产安卓系统会默认预置厂商定制字体,比如小米的 MiSans、vivo 的定制黑体,但这些字体多数不会以稳定名称暴露给网页,所以跨端最稳妥的思路还是把 Noto Sans CJK SC 或者思源黑体放到兜底位置。
一个容易被忽略的细节是:**不要给 font-family 的值加全局引号包裹整串字
