使用 font-display: swap 加载字体的网站,在加载网页字体并与后备字体交换时,通常会出现布局偏移 (CLS)。
要防止 CLS,您可以将后备字体的尺寸调整为与主要字体的尺寸相匹配。@font-face
规则中的 size-adjust
、ascent-override
、descent-override
和 line-gap-override
等属性有助于替换后备字体的指标,以便开发者更好地控制字体的显示方式。如需详细了解字体回退和替换属性,请参阅此博文。您还可以在此演示中查看此技术的有效实现。
本文介绍了如何在 Next.js 和 Nuxt.js 框架中实现字体大小调整功能,以生成后备字体 CSS 并降低 CLS。还演示了如何使用 Fontaine 和 Capsize 等交叉剪切工具生成后备字体。
背景
font-display: swap 通常用于防止 FOIT(不可见文本的闪烁)以及更快速地在屏幕上显示内容。swap
的值用于告知浏览器使用该字体的文本应立即使用系统字体显示,并且仅在自定义字体准备就绪时替换系统字体。
swap
的最大问题是令人不适的效果,即两种字体的字符大小不同会导致屏幕内容四处移动。这导致 CLS 得分较低,尤其是对于包含大量文本的网站。
下图显示了此问题的一个示例。第一张图片使用 font-display: swap
,未尝试调整后备字体大小。第二个示例展示了使用 CSS @font-face
规则调整尺寸如何改善加载体验。
不调整字体大小
body {
font-family: Inter, serif;
}
![文字的字体和大小突然发生变化,造成不协调的效果。](https://developer.chrome.google.cn/static/blog/framework-tools-font-fallback/image/text-suddenly-changes-f-ad5f294238b66.gif?authuser=0000&hl=lt)
调整字体大小后
body {
font-family: Inter, fallback-inter, serif;
}
@font-face {
font-family: "fallback-inter";
ascent-override: 90.20%;
descent-override: 22.48%;
line-gap-override: 0.00%;
size-adjust: 107.40%;
src: local("Arial");
}
![平滑过渡到其他字体的文本。](https://developer.chrome.google.cn/static/blog/framework-tools-font-fallback/image/text-transitions-smoothl-200e9965ef42b.gif?authuser=0000&hl=lt)
调整后备字体大小是防止字体加载布局偏移的有效策略,但从头开始实现该逻辑可能会比较棘手,如这篇关于字体回退的博文中所述。幸运的是,我们已经提供多种工具选项,可让您在开发应用时更轻松地做到这一点。
如何使用 Next.js 优化后备字体
Next.js 提供了一种内置方法来启用后备字体优化。当您使用 @next/font 组件加载字体时,此功能默认处于启用状态。
Next.js 版本 13 中引入了 @next/font 组件。该组件提供了一个 API,用于将 Google 字体或自定义字体导入您的网页,并包括内置的自动自托管字体文件。
使用时,系统会自动计算后备字体指标,并将其注入 CSS 文件中。
例如,如果您使用的是 Roboto 字体,通常可以在 CSS 中进行如下定义:
@font-face {
font-family: 'Roboto';
font-display: swap;
src: url('/fonts/Roboto.woff2') format('woff2'), url('/fonts/Roboto.woff') format('woff');
font-weight: 700;
}
body {
font-family: Roboto;
}
如需迁移到下一个/字体:
通过导入“Roboto”函数。函数的返回值将是类名称,您可以在组件模板中使用该名称。 请务必向配置对象添加
display: swap
才能启用此功能。import { Roboto } from '@next/font/google'; const roboto = Roboto({ weight: '400', subsets: ['latin'], display: 'swap' // Using display swap automatically enables the feature })
在您的组件中,使用生成的类名称:
javascript export default function RootLayout({ children }: { children: React.ReactNode; }) { return ( <html lang="en" className={roboto.className}> <body>{children}</body> </html> ); }
adjustFontFallback 配置选项:
对于 @next/font/google
:一个布尔值,用于设置是否应使用自动后备字体来减少 Cumulative Layout Shift。默认值为 true。Next.js 会根据字体类型(分别为 serif 和 sans-serif)自动将后备字体设置为 Arial
或 Times New Roman
。
对于 @next/font/local
:一个字符串或布尔值 false,用于设置是否应使用自动后备字体来减少 Cumulative Layout Shift。可能的值包括 Arial
、Times New Roman
或 false
。默认值为 Arial
。 如果您想使用 Serif 字体,不妨考虑将此值设置为 Times New Roman
。
Google 字体的另一个选项
如果无法使用 next/font
组件,可将此功能与 Google Fonts 搭配使用的另一种方法是通过 optimizeFonts
标志。Next.js 已默认启用 optimizationFonts 功能。此功能可将 Google Font CSS 内嵌在 HTML 响应中。此外,您还可以通过在 next.config.js 中设置 experimental.adjustFontFallbacksWithSizeAdjust
标记来启用字体回退调整功能,如以下代码段所示:
// In next.config.js
module.exports = {
experimental: {
adjustFontFallbacksWithSizeAdjust: true,
},
}
注意:目前没有计划通过新引入的 app
目录支持此功能。从长远来看,最好使用 next/font
。
如何使用 Nuxt 调整后备字体
@nuxtjs/fontaine 是 Nuxt.js 框架的一个模块,可自动计算后备字体指标值并生成后备 @font-face
CSS。
您可以通过在模块配置中添加 @nuxtjs/fontaine
来启用该模块:
import { defineNuxtConfig } from 'nuxt'
export default defineNuxtConfig({
modules: ['@nuxtjs/fontaine'],
})
如果您使用 Google Fonts 或者没有针对某种字体的 @font-face
声明,则可以将其声明为附加选项。
在大多数情况下,该模块可以从 CSS 中读取 @font-face
规则,并自动推断出字体系列、后备字体系列和显示类型等详细信息。
如果在模块无法发现的位置定义了字体,您可以传递指标信息,如以下代码段所示。
export default defineNuxtConfig({
modules: ['@nuxtjs/fontaine'],
fontMetrics: {
fonts: ['Inter', { family: 'Some Custom Font', src: '/path/to/custom/font.woff2' }],
},
})
该模块会自动扫描您的 CSS 以读取 @font-face 声明,并生成后备 @font-face 规则。
@font-face {
font-family: 'Roboto';
font-display: swap;
src: url('/fonts/Roboto.woff2') format('woff2'), url('/fonts/Roboto.woff') format('woff');
font-weight: 700;
}
/* This will be generated. */
@font-face {
font-family: 'Roboto override';
src: local('BlinkMacSystemFont'), local('Segoe UI'), local('Roboto'), local('Helvetica Neue'),
local('Arial'), local('Noto Sans');
ascent-override: 92.7734375%;
descent-override: 24.4140625%;
line-gap-override: 0%;
}
您现在可以在 CSS 中使用 Roboto override
作为后备字体,如以下示例所示
:root {
font-family: 'Roboto';
/* This becomes */
font-family: 'Roboto', 'Roboto override';
}
自行生成 CSS
独立的库也可以帮助您生成 CSS 代码,以便调整后备字体大小。
使用 Fontaine 库
如果未使用 Nuxt 或 Next.js,则可以使用 Fontaine。Fontaine 是为 @nuxtjs/fontaine 提供支持的底层库。您可以在项目中使用此库,通过 Vite 或 Webpack 插件自动注入后备字体 CSS。
假设您在 CSS 文件中定义了 Roboto 字体:
@font-face {
font-family: 'Roboto';
font-display: swap;
src: url('/fonts/Roboto.woff2') format('woff2'), url('/fonts/Roboto.woff') format('woff');
font-weight: 700;
}
Fontaine 提供 Vite 和 Webpack 转换器,可以轻松插入构建链,如以下 JavaScript 所示启用插件。
import { FontaineTransform } from 'fontaine'
const options = {
fallbacks: ['BlinkMacSystemFont', 'Segoe UI', 'Helvetica Neue', 'Arial', 'Noto Sans'],
// You may need to resolve assets like `/fonts/Roboto.woff2` to a particular directory
resolvePath: (id) => 'file:///path/to/public/dir' + id,
// overrideName: (originalName) => `${name} override`
// sourcemap: false
}
如果您使用的是 Vite,请按如下方式添加插件:
javascript
// Vite
export default {
plugins: [FontaineTransform.vite(options)]
}
如果使用的是 Webpack,请按如下方式启用它:
// Webpack
export default {
plugins: [FontaineTransform.webpack(options)]
}
该模块会自动扫描您的文件,以修改 @font-face 规则:
css
@font-face {
font-family: 'Roboto';
font-display: swap;
src: url('/fonts/Roboto.woff2') format('woff2'), url('/fonts/Roboto.woff') format('woff');
font-weight: 700;
}
/* This will be generated. */
@font-face {
font-family: 'Roboto override';
src: local('BlinkMacSystemFont'), local('Segoe UI'), local('Roboto'), local('Helvetica Neue'),
local('Arial'), local('Noto Sans');
ascent-override: 92.7734375%;
descent-override: 24.4140625%;
line-gap-override: 0%;
}
您现在可以在 CSS 中将 Roboto override
用作后备字体。
css
:root {
font-family: 'Roboto';
/* This becomes */
font-family: 'Roboto', 'Roboto override';
}
使用 Capsize 库
如果未使用 Next.js、Nuxt、Webpack 或 Vite,另一种方法是使用 Capsize 库生成后备 CSS。
新增了 createFontStack API
该 API 是名为 createFontStack
的 @capsize/core 软件包的一部分,它接受一系列字体指标,顺序与指定字体堆栈(font-family
属性)的顺序相同。
您可以参阅此处有关使用 Capsize 的文档。
示例
请考虑以下示例:所需的网页字体是 Lobster,回退到 Helvetica Neue,然后是 Arial。在 CSS 中,font-family: Lobster, 'Helvetica Neue', Arial
。
从核心软件包导入 createFontStack:
import { createFontStack } from '@capsizecss/core';
导入每种所需字体的字体指标(请参阅上文的字体指标):
javascript import lobster from '@capsizecss/metrics/lobster'; import helveticaNeue from '@capsizecss/metrics/helveticaNeue'; import arial from '@capsizecss/metrics/arial';`
创建字体堆栈,将指标作为数组传递,顺序与通过字体系列 CSS 属性传递的顺序相同。
javascript const { fontFamily, fontFaces } = createFontStack([ lobster, helveticaNeue, arial, ]);
此示例会返回以下内容:
{
fontFamily: Lobster, 'Lobster Fallback: Helvetica Neue', 'Lobster Fallback: Arial',
fontFaces: [
{
'@font-face' {
'font-family': '"Lobster Fallback: Helvetica Neue"';
src: local('Helvetica Neue');
'ascent-override': '115.1741%';
'descent-override': '28.7935%';
'size-adjust': '86.8251%';
}
'@font-face' {
'font-family': '"Lobster Fallback: Arial"';
src: local('Arial');
'ascent-override': 113.5679%;
'descent-override': 28.392%;
'size-adjust': 88.053%;
}
}
]
}
您必须将 fontFamily 和 fontFaces 代码添加到 CSS。以下代码展示了如何在 CSS 样式表或 <style>
代码块中实现此方法。
<style type="text/css">
.heading {
font-family:
}
</style>
这将生成以下 CSS:
.heading {
font-family: Lobster, 'Lobster Fallback: Helvetica Neue',
'Lobster Fallback: Arial';
}
@font-face {
font-family: 'Lobster Fallback: Helvetica Neue';
src: local('Helvetica Neue');
ascent-override: 115.1741%;
descent-override: 28.7935%;
size-adjust: 86.8251%;
}
@font-face {
font-family: 'Lobster Fallback: Arial';
src: local('Arial');
ascent-override: 113.5679%;
descent-override: 28.392%;
size-adjust: 88.053%;
}
您还可以使用 @capsize/metrics 软件包计算替换值,并自行将其应用于 CSS。
const fontMetrics = require(`@capsizecss/metrics/inter`);
const fallbackFontMetrics = require(`@capsizecss/metrics/arial`);
const mainFontAvgWidth = fontMetrics.xAvgWidth / fontMetrics.unitsPerEm;
const fallbackFontAvgWidth = fallbackFontMetrics.xAvgWidth / fallbackFontMetrics.unitsPerEm;
let sizeAdjust = mainFontAvgWidth / fallbackFontAvgWidth;
let ascent = fontMetrics.ascent / (unitsPerEm * fontMetrics.sizeAdjust));
let descent = fontMetrics.descent / (unitsPerEm * fontMetrics.sizeAdjust));
let lineGap = fontMetrics.lineGap / (unitsPerEm * fontMetrics.sizeAdjust));
致谢
主打图片,由 Alexander Andrews 在 Uncapture 上使用。