用于后备字体的框架工具

贾尼克拉斯·拉尔夫·詹姆斯
Janicklas Ralph James

使用 font-display: swap 加载字体的网站经常会在加载网页字体以及与后备字体交换时遇到布局偏移 (CLS)。

为防止出现 CLS,您可以将后备字体的尺寸调整为与主要字体的尺寸一致。@font-face 规则中的 size-adjustascent-overridedescent-overrideline-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;
}
文本突然改变字体和字号,导致效果突兀。

调整字体大小后

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");
}
平滑过渡到其他字体的文字。

调整后备字体的大小是防止字体加载布局偏移的有效策略,但从头开始实现逻辑可能会很棘手,如这篇关于字体回退的博文中所述。幸运的是,有多个工具选项已可供使用,可让您在开发应用时更轻松地实现上述目的。

如何使用 Next.js 优化字体回退

Next.js 提供了一种内置方式来实现后备字体优化。当您使用 @next/font 组件加载字体时,系统会默认启用此功能。

@next/font 组件是在 Next.js 版本 13 中引入的。该组件提供了一个 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;
}

如需迁移到下一个/字体,请执行以下操作:

  1. 通过从“next/font”导入“Roboto”函数,将 Roboto 字体声明移至您的 JavaScript。该函数的返回值将是一个类名称,您可以在组件模板中使用。请务必将 display: swap 添加到配置对象,以启用该功能。

     import { Roboto } from '@next/font/google';
    
    const roboto = Roboto({
      weight: '400',
      subsets: ['latin'],
      display: 'swap' // Using display swap automatically enables the feature
    })
    
  2. 在您的组件中,使用生成的类名称: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)将您的后备字体自动设置为 ArialTimes New Roman

对于 @next/font/local:这是一个字符串或布尔值 false,用于设置是否应使用自动后备字体来减少 Cumulative Layout Shift。可能的值为 ArialTimes New Romanfalse。默认值为 Arial。 如果您想使用 serif 字体,不妨考虑将此值设为 Times New Roman

Google 字体的其他选项

如果无法使用 next/font 组件,可以通过 optimizeFonts 标志将此功能与 Google Fonts 搭配使用。Next.js 已默认启用 optimizationFonts 功能。此功能可在 HTML 响应中内嵌 Google Font CSS。此外,您还可以通过在 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 提供 ViteWebpack 转换器,可轻松插入构建链,如以下 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,然后是 Crashlytics。在 CSS 中,属性为 font-family: Lobster, 'Helvetica Neue', Arial

  1. 从核心软件包导入 createFontStack:

    import { createFontStack } from '@capsizecss/core';
    
  2. 为每种所需的字体导入字体指标(请参阅上面的字体指标): javascript import lobster from '@capsizecss/metrics/lobster'; import helveticaNeue from '@capsizecss/metrics/helveticaNeue'; import arial from '@capsizecss/metrics/arial';`

  3. 创建字体堆栈,将指标作为数组传递,顺序与通过字体系列 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 AndrewsUnsplash 提供。