☰
CSS字体栈与@font-face实战:中文字体加载回退与排版优化
2026/10/31 7:42:38 网站建设 项目流程

样式表里最不起眼、又最容易翻车的一行,大概就是font-family了。写页面的时候谁都顺手敲过sans-serif,可真到了线上,字体发虚、字重错乱、中英文基线对不齐、首屏字体闪烁、换个系统就变成另一副模样,这些问题一个接一个冒出来。样式表中常用字体这个选题看着基础,实际上它牵扯到字体栈的匹配规则、浏览器逐字符回退机制、自定义字体的加载渲染链路、中文字体的体积现实,以及字号行高字距字重这一整套参数的联动。它解决的不是"把字显示出来"这么浅的需求,而是让同一份样式在不同系统、不同设备、不同网络条件下都能稳定地显示出该有的气质。下面这些内容适合刚接触前端排版的新手建立起正确的心智模型,也适合已经在写组件库、设计系统、内容站点的老手拿来对一遍自己的实现细节,尤其是中文站点这一块,很多照抄英文方案的坑我都踩过。

1. 把 font-family 当成一份候选人名单来理解

我第一次认真读字体规范的时候,最大的认知颠覆是:font-family不是"指定一个字体",而是"提交一份按优先级排序的候选人名单"。这个视角一换,后面所有的问题都好解释了,包括为什么字体栈最后总要挂一个通用字族,为什么有时候明明写了自定义字体却还是显示成系统默认,为什么同一段文字里英文和中文会来自两套完全不同的字体。

1.1 浏览器挑字体的真实顺序

浏览器拿到font-family: A, B, C, sans-serif之后,做的事情大致是三步。第一步,把 A、B、C 逐个拿去和"当前可用的字体集合"比对,这个集合包括操作系统本地安装的字体,以及在样式表里通过@font-face声明且成功加载的字体。第二步,选中第一个命中的字体作为主力字体。第三步,也是最容易被忽略的一步——渲染每个字符时,如果主力字体里缺少这个字形的定义,浏览器不会就此放弃,而是继续沿着名单往后找,直到某个字体能提供这个字形为止,最后实在找不到才落到通用字族或者系统兜底字体(俗称"豆腐块"的那个方框)。

这个"逐字符回退"的机制解释了很多现象。比如你只给英文配了一款很讲究的拉丁字体,中文没有单独指定,页面上的汉字不会变成方框,而是自动回退到系统的中文字体去,视觉上就出现了"英文很精致、中文很普通"的割裂感。反过来,如果你把一个只有中文字形的字体放在名单第一位,那么英文部分也会用它,而大多数中文字体内置的拉丁字形设计是相当粗糙的。

注意:命中不等于可用。如果名单第一位的字体是通过@font-face引入但还在下载中,浏览器的处理方式取决于font-display的设置,这时候会出现"先看到兜底字体、后切换"或者"一直等到超时"的不同表现,这部分后面单独讲。

1.2 通用字族到底指向什么

通用字族(generic family)是名单里唯一不带引号、也不对应具体字体文件的关键字,它由浏览器自己决定映射到哪个真实字体。常见的几个:

关键字典型映射倾向适用场景
serif衬线体,正文类长文阅读、杂志风格
sans-serif无衬线体,界面类UI、后台、移动端
monospace等宽体代码、数字对齐、日志
cursive手写风装饰性标题,慎用于正文
fantasy装饰体极少用,各平台差异极大
system-ui当前系统的界面字体现代 Web 应用首选

system-ui是这几年用得最多的一个,它的好处是自动跟随操作系统:在桌面端拿到的是系统界面字体,在移动端拿到的是手机默认黑体,用户看起来会觉得"这个网站和我的系统是一伙的",而不是"从别处搬来的"。它的代价是不同平台的渲染结果不一样,如果你对像素级一致性有硬要求,就不能完全依赖它,得靠自定义字体。

我想强调一个特别常见的写法错误:把通用字族放在名单中间。比如font-family: sans-serif, "Brand Sans",这个写法在语义上等于"后面的都不要了",因为sans-serif在几乎任何环境下都能命中,后面的自定义字体永远轮不到。通用字族只能放在名单最后一位,这是硬规则。

1.3 引号和拼写这些细枝末节

字体名里包含空格、数字开头、或者含特殊字符的时候必须加引号,比如"PingFang SC"、"Source Han Sans SC"。单个单词构成的字体名严格来说可以不加引号,但我建议统一加,理由是团队协作时没人记得住哪几个需要加哪几个不需要,统一加引号不会有任何副作用,而漏加引号导致的匹配失败排查起来非常费时间。

拼写错误是另一个高频事故点。字体名对大小写不敏感,但对空格和连字符敏感,SourceHanSans和Source Han Sans是两个完全不同的名字,前者匹配不上任何东西,浏览器不会报错,只会静默地往后回退,于是你在开发机上看到的是系统默认字体,还以为是自己样式没生效。这类问题我一般用浏览器开发者工具的"计算样式"面板去核对,它会直接告诉你最终生效的是哪个字体文件,比盲猜快得多。

2. 中文站点的字体栈:不能照抄英文那一套

英文站长年累月总结出来的那套"引一款 Google Fonts 里的拉丁字体"的做法,挪到中文场景基本行不通。原因很朴素:字形数量不是一个量级。

2.1 为什么中文字体不能全靠网络加载

一套西文无衬线字体,覆盖拉丁字母、数字和常用符号,字符集通常在几百个字形左右,压缩成 woff2 也就几十 KB,随便加载不心疼。而一套中文黑体,常用汉字加标点符号至少也要六七千个字形起步,完整覆盖 GB18030 的话是几万个字形,即使压成 woff2 也常常是几 MB 到十几 MB。这个体积放在首屏,等于给每一个用户强行塞了一次大文件下载,弱网环境下直接就是几秒钟的白屏或者字体闪烁。

所以中文站点的默认策略应该是:能用系统字体就用系统字体。系统字体是随操作系统预装的,不需要下载,渲染也经过系统层的优化,虽然不同平台长得不一样,但起码都是各家的"正经黑体",观感上不会太差。真正需要自定义中文字体的场景,一般是有强烈品牌诉求的标题字、活动页、海报类页面,而且往往只用在少量文字上,这时候再用子集化的方式把体积压下来。

2.2 一套能直接用的中英文混排字体栈

下面这套是我目前项目里用得最顺手的一版,逻辑是"英文优先走系统界面字体,中文按平台各自的默认黑体依次回退,最后兜底无衬线":

:root { --font-sans: system-ui, "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", "Source Han Sans SC", "WenQuanYi Micro Hei", sans-serif; --font-serif: Georgia, "Songti SC", "SimSun", "Noto Serif SC", serif; --font-mono: ui-monospace, "SFMono-Regular", Consolas, "Liberation Mono", monospace; } body { font-family: var(--font-sans); }

这套栈的设计思路值得拆开说。system-ui打头,意味着在绝大多数现代系统上,英文和数字会用上系统界面字体,观感干净、字重齐全,也不用额外下载。紧跟其后的几个中文字体是按平台优先级排的,不同系统只会命中自己装了的那个,命不中的自动跳过,不会产生额外开销。名单末尾的sans-serif是保险丝,防止某些精简系统一个中文字体都没有的极端情况。

同样的思路,衬线栈里我把 Georgia 放在最前面,它在多数系统上都有,衬线风格的拉丁字形识别度很高,中文则回退到宋体类字体。等宽栈用了ui-monospace这个较新的关键字,能拿到系统自带的等宽字体,比硬写某个具体字体名更稳。

提示:如果你的产品需要在 Linux 桌面环境下也有体面的中文显示,名单里务必保留一两个开源中文字体名,比如思源黑体、文泉驿系列,很多精简发行版默认只装了其中之一。

2.3 中英文混排的字号与字重补偿

字体栈排好只是第一步,真正让中文看起来舒服的是参数补偿。同样是16px,拉丁字母的视觉尺寸明显小于汉字,因为汉字是满格设计,字面率高,而拉丁字母有大量的留白和升降部空间。这就是为什么很多页面在纯英文下看着清爽,一换成中文就觉得"字太大了、挤得慌"。

我的处理方式是给中文正文单独降一档字号。比如英文正文用16px,中文正文用15px或者0.9375rem,行高比英文再抬高一点。如果你用的是支持size-adjust的可变字体或者自定义字体,可以在@font-face里直接写:

@font-face { font-family: "Brand Sans"; src: url("./fonts/brand-sans.woff2") format("woff2"); size-adjust: 96%; font-display: swap; }

size-adjust的作用是把这款字体整体按比例缩放,相当于在不改font-size的前提下做了一次视觉校准。它在做兜底字体对齐的时候尤其有用——你可以让兜底字体的视觉尺寸尽量逼近自定义字体,从而把字体切换时的布局抖动(CLS)压到最低。

字重这块,中文系统的现实很骨感:大多数系统自带的黑体只提供 Regular 和 Bold 两档,甚至只有一档。你在 CSS 里写font-weight: 500,浏览器找不到 500 的字形,会按规范的回退算法去找最接近的可用字重,结果往往是 400 或者 700,也就是你精心设计的"半粗标题"直接变成了普通字重或者过粗的合成粗体。合成粗体是浏览器用描边算法硬凑出来的,边缘发毛、笔画粗细不均,在小字号下特别难看。我的做法是中文标题要么用 400 靠字号拉开层级,要么明确用 700,中间档位尽量不用。

3. @font-face 的完整链路:从文件格式到渲染结果

自定义字体是排版质感提升最明显的一步,也是最容易出性能事故的一步。这条链路上有四个决策点:格式、体积、加载策略、渲染兜底,任何一个选错都会在线上留下痕迹。

3.1 格式选择与体积账怎么算

字体文件的常见格式有这么几种,我按推荐顺序排:

格式压缩率浏览器支持建议
woff2最好,比 woff 再小约三成现代浏览器全覆盖首选,唯一必选
woff中等老版本浏览器兜底可选,做兼容时保留
ttf/otf差,几乎不压缩全支持但体积大只在调试时用
eot差早已淘汰不要再写

只写 woff2 在今天的浏览器覆盖率下是完全可以接受的,src里堆四五个格式主要是历史包袱。如果你确实要兼顾很老的客户端,写成降级形式即可:

@font-face { font-family: "Brand Sans"; src: url("./fonts/brand-sans.woff2") format("woff2"), url("./fonts/brand-sans.woff") format("woff"); font-weight: 400; font-style: normal; font-display: swap; }

这里有个细节值得说:src里的多个候选是"取第一个能成功加载的",不是"全部下载"。所以把 woff2 放前面,支持它的浏览器根本不会去碰后面那个 woff 文件,不会造成双倍流量。

3.2 font-display 五个取值的真实体感

font-display决定了字体还没下载完的那段时间里,页面该怎么显示,这是字体体验的核心开关。规范里定义了五个值,我按实际观感排一下:

  • auto:交给浏览器自己决定,各浏览器实现不一致,实际上等于不设防,不建议。
  • block:先短暂隐藏文字(通常约 3 秒),等字体来了再显示。文字会"消失一下再出现",用户会觉得页面卡了。
  • swap:立刻用兜底字体显示,字体到了再替换。首屏永远有内容,代价是会有一次视觉跳变。
  • fallback:折中方案,隐藏极短时间,之后用兜底字体,给字体一个较短的交换窗口,超时就放弃。
  • optional:最激进的性能方案,如果字体没能在极短时间内到达,本次访问就直接用兜底字体,不再切换。

我的选择原则很直接:正文用swap,因为内容可读性优先,用户不能等;品牌标题、Logo 字体用block或fallback,因为视觉一致性比几毫秒的延迟重要;广告位、次要装饰字体用optional,加载不到也无所谓。

用swap必须配套做一件事:让兜底字体和自定义字体的视觉尺寸尽量接近。否则字体一切换,整段文字的行高、宽度全变,页面会像被人推了一把。手段就是前面提到的size-adjust,再配合ascent-override、descent-override、line-gap-override这几个描述符微调垂直度量。这几个属性看着冷门,但做过设计系统的团队基本都会配一套,配置好之后字体切换的抖动几乎看不出来。

3.3 子集化与 unicode-range 分片实操

中文自定义字体绕不开体积问题,解决路径有两条:子集化和分片。

子集化的意思是,把字体文件里用不到的字形直接删掉,只保留页面真正会用到的字符。如果你的站点标题是固定的几句话,比如品牌名、栏目名,那子集化之后体积可能只有几十 KB。工具链上用pyftsubset很顺手:

pyftsubset SourceHanSansSC-Regular.otf \ --text-file=chars.txt \ --flavor=woff2 \ --layout-features='*' \ --no-hinting \ --desubroutinize \ --output-file=brand-subset.woff2

chars.txt里放上所有需要的字符,一行或者一整个字符串都行。--layout-features='*'保留全部排版特性,中文标点的挤压、连字这些都会受影响,不要随手砍。--no-hinting可以再省一点体积,现代屏幕上 hinting 的意义已经不大。

分片的思路不一样,它不删字形,而是把一个大字体切成若干个小文件,用unicode-range告诉浏览器"这段字符去这个文件取"。浏览器只会下载当前页面实际用到的分片,等于把下载变成了按需。常见做法是按字符使用频率切片,把最常用的两三千字放进第一个分片,剩下按区间切。

@font-face { font-family: "Brand Sans"; src: url("./fonts/brand-0.woff2") format("woff2"); unicode-range: U+4E00-9FFF; font-display: swap; } @font-face { font-family: "Brand Sans"; src: url("./fonts/brand-latin.woff2") format("woff2"); unicode-range: U+0000-00FF, U+2000-206F, U+3000-303F; font-display: swap; }

这里的细节是:多个@font-face可以共用同一个font-family名字,浏览器会根据unicode-range自动决定哪段字符去哪个文件取。中文标点(U+3000-303F)单独切一片是个好习惯,因为标点的字形设计和汉字不一样,标点分片往往能做得非常小。

3.4 可变字体值不值得上

可变字体(Variable Fonts)把多个字重、字宽甚至倾斜角度塞进一个文件,用一条轴上的连续数值来控制,而不是每档字重一个独立文件。它对中文场景的诱惑很大——一套中文黑体的 Regular 加 Bold 两个文件可能是十几 MB,而一个可变字体文件只比单个静态文件大一点点。

代价是浏览器渲染可变字体时的性能开销略高,而且中文可变字体目前市面上可选的成品还不多,很多是从静态字体转出来的,质量参差不齐。我的判断标准是:如果你的设计需要用超过三档字重,可变字体划算;如果只需要常规和粗体两档,老老实实用两个静态子集文件更省心。

用法上,声明的时候用字重区间:

@font-face { font-family: "Brand Var"; src: url("./fonts/brand-var.woff2") format("woff2-variations"); font-weight: 100 900; font-display: swap; }

然后正常的font-weight: 550就能生效了,浏览器会在轴上插值。如果需要控制字重的其他维度,比如字宽轴、光学尺寸轴,用font-variation-settings:

.headline { font-family: "Brand Var"; font-weight: 700; font-variation-settings: "wght" 700, "wdth" 95, "opsz" 32; }

注意:font-variation-settings里的轴标签是四个字符的简写,写错了不会报错,只会静默失效。而且它的优先级高于font-weight,两者同时写的时候以它为准,调试时要留意这个顺序。

4. 排版参数联动:字号、行高、字距、字重

字体选对了,参数没调好,出来的效果依然会很业余。这四个参数不是独立的,它们互相牵制,改一个往往要跟着动另一个。

4.1 行高为什么必须写无单位数值

line-height可以写无单位数值、可以写px、可以写百分比、也可以写em,但只有无单位数值是安全的。原因在于继承规则:无单位数值在继承时传递的是"倍数"这个因子,子元素会用因子乘以自己的字号重新计算;而px和em、百分比在继承时传递的是"计算后的绝对值",子元素直接拿这个固定值用,字号一变行高就不对了。

/* 推荐 */ body { font-size: 16px; line-height: 1.6; } /* 有隐患:子元素 font-size 变了,行高不会跟着变 */ body { font-size: 16px; line-height: 25.6px; }

至于具体取多少,我的经验是按语言分开:纯英文正文1.4到1.6就够,因为拉丁字母本身有较多的垂直空间;中文正文我会拉到1.7到1.8,因为汉字是方块结构,字面率高,行与行之间如果太挤,整体会像一块实心的墨,读起来很累。标题相反,中文标题行高可以压到1.2到1.3,因为标题往往一行就结束,压紧了视觉更有块面感。

还有一个容易被忽略的点:行高的基准是font-size,而font-size的基准可能是rem,rem又依赖根元素的字号,根元素的字号又会被用户浏览器的偏好设置覆盖。所以如果你在根元素上写了html { font-size: 62.5% }这种技巧,用户的浏览器最小字号设置会直接打乱你的比例。我的做法是根元素保持浏览器默认的100%,用rem表达设计稿尺寸,这样无障碍和响应式两头都不吃亏。

4.2 字号阶梯的计算

字号不是随便排几个数字就行,成体系的字号应该来自一个比例序列,也就是常说的"模块化尺度"。做法是选一个基准字号,再选一个比例系数,逐级相乘。

常用的比例有1.125(轻微)、1.2(克制)、1.25(明显)、1.333(强烈)、1.5(夸张),分别对应不同的气质。实例算一下:基准取16px,比例取1.25,那一级就是20px,两级是25px,三级是31.25px,反过来小一级是12.8px,小两级是10.24px。

这里会撞上一个现实约束——12.8px的中文在部分浏览器和系统的最小字号限制下会被强制拉到12px以上,10.24px的中文基本已经失去了可读性。所以照抄比例序列之后必须做一轮截断处理:低于12px的级别直接砍掉,或者并入上一级。我一般只在12px到48px这个区间里保留五到六个级别,再大就交给标题用clamp()做流体响应。

:root { --fs-xs: 0.75rem; /* 12px */ --fs-sm: 0.875rem; /* 14px */ --fs-base: 1rem; /* 16px */ --fs-md: clamp(1.25rem, 1.1rem + 0.6vw, 1.5rem); --fs-lg: clamp(1.5rem, 1.2rem + 1.2vw, 2rem); --fs-xl: clamp(2rem, 1.4rem + 2.4vw, 3rem); }

clamp()的中间那个表达式负责在视口变化时做线性插值,最小值和最大值负责兜住两端,这样字号在移动端不会小到看不清,在宽屏上也不会大到失控。这套写法比写一堆媒体查询要清爽得多。

4.3 字距与字重的微调经验

中文正文默认是不需要加字距的,因为汉字本身就是等宽方块,字与字之间天然留白。但在两种情况下我会手动加一点:一是大字号标题,汉字放大后笔画之间的空隙相对变小,加个0.02em到0.04em会显得通透;二是全大写英文或者数字串,字母之间的默认间距在放大后会显得过紧,加0.01em到0.05em更舒服。

反方向的操作也有:超大标题(比如48px以上)中文可以考虑负字距-0.02em,让字形成整体,视觉更紧凑。但这个值的判断非常依赖具体字体,我一般是在浏览器里直接拖数值试,试到一个"看起来不散"的点就停。

字重那边,我想再强调一次"合成"这回事。浏览器在两种情况下会自己动手伪造字形:字体没有粗体但有font-weight: 700的需求时合成粗体,字体没有斜体但有font-style: italic的需求时合成斜体。合成的结果在显示器上通常是笔画糊、边缘脏。如果你的设计里确实不需要粗体和斜体,可以在根元素上直接关掉:

body { font-synthesis: none; }

关掉之后,浏览器找不到字重就会老老实实回退到最接近的可用字重,不会再硬凑一个出来。这个属性对中文排版尤其友好,因为中文的合成粗体问题比英文严重得多。

5. 字体相关的典型故障与排查实录

字体问题有个共同特点:不报错。样式不生效、字体没加载、字重被替换,控制台里一个字都不会说。所以排查必须靠固定的顺序,不能靠猜。

5.1 字体没生效的排查顺序

我自己的固定动作是按下面这个顺序走,基本能在几分钟内定位:

先看计算样式。开发者工具里选中元素,看最终生效的font-family是什么,再看渲染字体那一栏显示的是哪个实际字体文件。这一栏如果显示的是系统兜底字体,说明自定义字体压根没参与进来,问题在加载环节;如果显示的是自定义字体,说明加载没问题,问题在参数或者字形覆盖上。

再查网络面板。筛选字体类型,看请求有没有发出去。没发出去一般是路径写错、src里的url()引号形式有问题、或者被 CSP 的font-src指令拦了。发出去但状态码是 404,那就是路径或者构建产物的文件名哈希对不上。状态码 200 但控制台有跨域报错,那是字体文件的响应头缺了Access-Control-Allow-Origin,跨域字体必须带上这个头,这一点经常被忽略,因为图片和脚本跨域往往没这么严格。

最后核对format()的写法。format("woff2")和format("woff2-variations")是两个不同的标识,写错了浏览器会认为格式不认识从而跳过这个候选,继续找下一个,如果只有一个候选就整体失败。

5.2 加载性能与闪烁问题

字体造成的性能问题,表现形式主要是三种:首屏文字延迟出现、字体切换导致的布局抖动、以及大字体文件拖慢整体加载。

第一种靠font-display: swap就能基本解决,让兜底字体先顶上。第二种靠size-adjust和几个度量覆盖属性把兜底字体调到和自定义字体视觉接近。第三种最狠,得从源头减体积:子集化、分片、只保留必要格式。

还有一个实用手段是给关键字体加预加载,让浏览器在解析 CSS 之前就开始下载:

<link rel="preload" as="font" type="font/woff2" href="/fonts/brand-latin.woff2" crossorigin>

crossorigin这个属性不能漏,字体请求默认是跨域模式,漏了它浏览器会重复下载一次,等于白预加载。另外预加载要克制,只给首屏真正用到的那一两个字体文件加,给五六个字体都加预加载,反而会抢走关键资源的带宽。

注意:预加载是一种"我确定这个资源马上要用"的声明,加错了比不加更糟。上线前最好在弱网模拟下看一眼网络瀑布图,确认预加载的字体确实在首屏被用到了。

5.3 常见问题速查表

现象大概率原因处理方式
字体完全没变化路径错、拼写错、format()不匹配看网络面板是否有请求,核对字体名字符
控制台报跨域缺Access-Control-Allow-Origin在字体所在服务上加响应头
粗体看起来发糊字体无该字重,浏览器合成font-synthesis: none或补齐字重文件
斜体变形怪异合成斜体同上,或改用oblique配合真实斜体文件
首屏文字空白font-display: block或未设置改swap,配合兜底字体度量校准
字体切换时页面跳动兜底字体与自定义字体尺寸差异大size-adjust+ascent-override等
中文显示成方框字体不含该字符且无兜底字体栈末尾补sans-serif
移动端字号偏小视口未设置或用了固定px缩放用rem+clamp(),检查 viewport
部分标点位置怪标点来自不同字体用unicode-range把标点分片单独指定
字体在某个系统上很虚该系统渲染策略差异谨慎使用文字平滑相关属性,优先调字重

还有一类问题不在表里,但很常见:中文标点在两端对齐或者等宽布局下导致列对不齐。原因是中文标点在字体里通常占据半个字符宽度,和汉字的整字符宽度不一致,如果容器宽度没算好就会出现一列宽一列窄。解决思路是给标点单独做unicode-range分片,或者干脆在需要严格对齐的场景(比如表格里的数字列)换成等宽字体栈。

5.4 几个我踩过的坑

第一个坑是"开发机好看、线上难看"。我在本地用了一款系统里装着的字体测试,效果很满意,结果上线后大量用户看到的完全是另一个样子,因为那款字体只是我本机装了,用户的系统里没有。教训是:只要字体名不是通用字族,就必须假定用户机器上没有,要么走@font-face引入,要么在字体栈里准备好足够的后备。

第二个坑是font-weight的无级写法。设计稿里标了550,我照着写了,本地因为用的是可变字体所以完美生效,但线上某些环境回退到了静态字体,550被解析成了400,整个页面的层级感全塌了。后来我统一规定:非可变字体的场景只用400和700两档,中间值一律不用。

第三个坑是字体加载和骨架屏打架。页面用骨架屏占位,字体用swap加载,结果字体切换的那一刻骨架屏刚好消失,两件事叠在一起,视觉上像是页面闪了一下。后来的做法是让骨架屏的消失时机跟字体加载状态解耦,骨架屏只等数据,不等字体,反而更稳。

第四个坑最隐蔽:某个组件库内部写死了font-family: Arial, sans-serif,导致整个组件里的中文全部回退到系统默认,和站点的其他部分对不上。排查了半天才发现是组件库的样式优先级压过了全局设置。这类问题只能靠统一的 CSS 变量管理字体栈,禁止在组件里直接写字体名,从制度上堵住。

我个人在实际项目里的体会是,字体这块最值钱的不是记住多少个字体名,而是建立三个习惯:一是所有字体声明都走 CSS 变量,全局只有一处定义;二是字体栈永远以通用字族收尾,永远给兜底留位置;三是每引入一个自定义字体,都同步想清楚它的体积、加载策略和兜底校准,三件事一起做完再提交。这三点做到了,字体相关的线上事故基本能压到接近于零。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询