设计稿里的标题用了某款黑体,本地起服务预览一切正常。上线之后,安卓机上是系统默认字,iPhone 上有的机型正常、有的不对,控制台里还挂着一串红色报错。同一个页面、同一份 CSS,表现却随设备变。
字体加载是条链子:文件请求、跨域许可、字节量、回退策略,四层里任何一层断了,结果都是“看起来没生效”。按这四层从下往上查,每层都有能直接看到的现象。
一、第一层:文件到底请求到了没有
先看 Network 面板。 打开开发者工具,切到 Network,按 Font 过滤,刷新页面。三种结果对应三个方向:
- 没有请求:
@font-face没被解析,或者这段 CSS 没被任何元素用到,样式表被后面的规则覆盖了。 - 请求返回 404:路径错了。构建工具改了文件名、CDN 上没同步、大小写不一致,都会走到这一步。
- 请求 200 但字体没生效:往下看第二层和第三层。
路径问题比想象中多。 带 hash 的构建产物,文件名每次发布都变,CSS 里手写的路径跟不上;放在 CDN 上的字体,本地开发走的是相对路径,上线后域名换了却没改。
family 名要对得上。@font-face 里写的 font-family 和使用处写的那串名字必须一字不差,含空格和引号。写中文名和英文名两套更容易出错,建议统一用英文别名,使用处只写别名。
用脚本确认一次,比猜快。 在控制台跑 document.fonts.check('16px "your-family"'),返回 true 说明字体已就绪,返回 false 说明还没加载或者加载失败。再用 document.fonts.load('16px "your-family"') 手动触发一次,失败时会给出具体错误。
格式声明别写错。src 里的 format() 要和真实格式一致,woff2 写成 woff 会被跳过。多格式并列时把 woff2 放前面,老浏览器会自己往后找。
二、第二层:跨域与服务器配置
字体请求是跨域请求。 浏览器对字体的加载比图片严格:只要字体文件和页面不同源,响应里就必须带 Access-Control-Allow-Origin,并且请求本身是以 CORS 模式发出的。缺这个头,控制台会明确写出被 CORS 策略拦截,字体不会加载。
这一条在 CDN 上最常踩。 图片走 CDN 没问题,字体走同一个 CDN 就被拦,因为图片不受这条限制。修法有两条:把字体放回同源域名下,或者在 CDN 上给字体文件加跨域响应头,允许你的站点域名。
crossorigin 属性别漏。 用 preload 预加载字体时,标签上要带 crossorigin,否则预加载的那次请求和实际使用的那次对不上,等于白预加载一次,还会多一次重复下载。
MIME 类型要对。 woff2 是 font/woff2,woff 是 font/woff,ttf 是 font/ttf。有些服务器把字体当二进制流返回 application/octet-stream,多数浏览器能容错,但在严格模式下会被拒绝。看着像“偶发不生效”的问题,有时就出在这里。
用 file:// 打开页面一定失败。 本地双击 HTML 文件预览,源是 null,字体请求必然被拦,调试时起一个静态服务。
协议也要一致。 页面走 https,字体走 http,会被当成混合内容拦掉。
三、第三层:体积与子集化
中文字体的体积是硬约束。 一个全量中文 ttf 常在 5 到 10MB,转成 woff2 后仍有 1.5 到 4MB。手机在 4G 下的实际下载速度按每秒 1 到 3MB 算,一个 3MB 的字体要一秒以上,弱网要五六秒。这段时间里页面只能用回退字体显示。
判断标准很简单:首屏字体控制在 300KB 以内。 超过这个量级,就要做子集化或者分片。
子集化分两种做法。 一种是发布前按页面实际用到的字符生成子集,通常能压到 200 到 400KB,适合文案固定的落地页;另一种是按 unicode-range 把全量字体切成几十到上百片,浏览器只下载当前页面出现的那几片,适合内容经常变的站点。
unicode-range 要按实际用字排。 中文常用字集中在前几千个码位,按使用频率切片比按码位等分有效得多。切片太碎会增加请求数,HTTP/2 下几十个请求可以接受,上百个就该合并。
字体只用一款就够。 首屏同时加载三款中文,字节量翻三倍。层级差用同一家族的不同字重拉开,或者干脆用字号与颜色区分。思源黑体 这类开源字体提供网页格式,字形统一,做正文与标题都够用。
压缩交给服务器。 woff2 本身已内置压缩,不必再叠一层 gzip;woff 和 ttf 可以开 gzip。开了反而变大,说明压缩策略配错了。字体文件名带 hash 的,缓存时间可以设长一些,免得每次访问都重新付一次体积成本。
四、第四层:回退链与 font-display
回退链要写全。font-family 里依次列:自定义字体、系统黑体、通用族。
font-family: "your-family", "PingFang SC", "Microsoft YaHei", sans-serif;
只写自定义字体一项,字体没加载时浏览器会退到默认衬线,版式立刻走形。补上两三个中间层,至少有兜底。
中文名和英文名都写。 同一个字体在不同系统里的注册名不一样,PingFang SC 在 macOS 上有效,Windows 上要靠 Microsoft YaHei。两个都列上,覆盖面才够。
回退字体的度量差异会造成跳动。 自定义字体和回退字体的字宽、行高不一样,字体加载完成后文字重排,页面会抖一下。用 size-adjust、ascent-override、descent-override 在 @font-face 里把回退字体的度量调成接近值,能压掉大部分位移。
font-display 决定加载期间显示什么。 常用四个值:
block:先空白,最多等三秒,之后用回退字体;swap:立刻用回退字体,字体到位后替换,适合正文;fallback:短暂空白,超时后一直用回退字体,不再替换;optional:给很短的时间窗口,没下完就放弃,本次访问不再换。
正文用 swap,标题可以用 optional。 正文怕读不到,宁可先显示相近的字;大标题怕跳动,弱网下直接用回退字体反而更稳。
自检方法。 把网络限速调到 3G,勾上禁用缓存,刷新页面看首屏:字先出来、后换字体,说明回退链生效;一直空白,说明 font-display 设成了 block,或者用了隐藏文本的写法。
最后核对一遍。 用 MiSans 或 抖音美好体 这类字面偏大的字体做标题时,回退字体的字宽差更明显,size-adjust 的值要单独调,别直接套用别的字体算出来的数。
五、授权说明
网页字体是最容易被忽略的一栏:把字体文件放到服务器上供浏览器下载,本身就是一次复制与分发。核验按三步走。
第一步,确认字体全名。 同一系列常分多个字重、多个字符集版本,网页用的往往是子集版,与桌面版不是同一份文件。带 GB、GBK、VF 后缀的要单独看。查协议前先把全名和版本写下来。
第二步,查协议类型。 常见几类:开源协议(SIL OFL、MIT、GPL)、作者声明免费商用、随包附带的免费商用声明、带保留字体名称条款的授权。开源协议通常明确允许网页嵌入,但要求保留版权声明;作者声明类的范围由声明自身界定,很多只覆盖平面与视频,不覆盖网页字体服务。猫啃网收录的 839 款免费中文字体,协议类型在下载页有标注,逐款对照即可。
第三步,核使用场景。 自建站点的页面展示、第三方平台的店铺页、小程序、App 内嵌页,性质不完全一样。把字体文件传到第三方平台的服务器上由其分发,属于向第三方提供字体文件,多数作者声明不覆盖这一项,要单独确认。
留凭证。 字体全名、版本、获取日期、许可页面截图、上线日期记在同一份文档里。字体换版本后要重新核一遍,别拿旧截图当依据。
六、FAQ
问:本地预览正常,上线后字体全没了,最可能是什么原因?
先看跨域。本地是同源,上线后字体在 CDN 或另一个域名上,响应里缺 Access-Control-Allow-Origin,浏览器直接拦掉。控制台会写得很清楚,按提示加响应头即可。
问:控制台没报错,Network 里字体也是 200,为什么字还是不对?
看 font-family 名字是否一致,再看 format() 声明是否与实际格式相符。还有一种情况是字体文件本身被裁剪过,缺了页面用到的字,那几个字会单独回退,看起来像“部分字不对”。
问:字体换成 woff2 之后还是慢,怎么办?
woff2 只是压缩,不减少字符数。全量中文字体即使压过仍有几 MB,要么按文案子集化,要么按 unicode-range 分片,只下当前页面用到的部分。
问:字体加载完成后页面跳一下,能避免吗?
能减轻,不能完全消除。用 size-adjust 与 ascent-override 调整回退字体的度量,让两者占位接近;标题类文字可以用 font-display: optional,弱网下直接不换字。
小结
H5 字体加载不出来,按四层查:先在 Network 与 document.fonts.check 里确认文件请求到了没有,再看跨域头、MIME 与协议是否放行,然后算体积,用子集化或 unicode-range 把首屏压到 300KB 以内,最后补齐回退链与 font-display。四层都通,字体才会在各类设备上稳定出现。授权这一栏单独核:全名、类型、场景,尤其是把字体交给第三方平台分发的情况,凭证留好再上线。
猫啃网

