✂️ Nuxt 最简单的字体裁剪工具:Fontize

做中文网站的人,多少都纠结过字体这件事。

想用一款好看的中文字体,比如思源宋体,源文件十几 MB。直接让用户下载显然不现实,于是大多数时候只能退回系统默认字体,接受各平台渲染效果参差不齐。

一个页面实际渲染的字符,可能只有几百个。为了一个几百字的页面,加载一个包含两万多个字的字体文件,这里面的浪费是三个数量级。

子集化:只打包用到的字

这个思路叫字体子集化(font subsetting):把字体文件裁剪到只包含实际用到的字符。工具链其实一直存在,比如 pyftsubset、subset-font。真正的麻烦不在"裁",而在"怎么知道页面用了哪些字":

  • 手动维护一份字符清单,注定漏字;
  • 文案一改,清单要重新整理,字体要重新裁;
  • 接口返回的动态文本,构建期根本拿不到。
  • 3500 常见字清单,中规中矩。

fontize 想解决的就是这一步:让组件自己声明会渲染什么文本,工具链负责收集和裁剪,开发时实时生效,构建时自动产出。

用法

fontize 是一个 pnpm monorepo,要求 Nuxt 4.5+。

第一步,安装并注册字体:

ts 复制代码
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@fontize/nuxt'],
  fontize: {
    fonts: [
      { alias: 'noto-serif', src: './fonts/NotoSansSC.ttf' },
      // 同别名多字重共享同一字符集
      { alias: 'noto-serif', src: './fonts/NotoSansSC-Bold.ttf', weight: 700 },
    ],
  },
})

第二步,在组件里声明文本:

vue 复制代码
<script setup lang="ts">
import { useText } from '@fontize/vue'

// 静态文本:返回值即 ref,模板自动解包
const text = useText('静态标题文本', 'noto-serif')

// 异步数据:传 getter,数据就位后 fontize 会重估终态文本
const { data } = useAsyncData('txt', () => $fetch('/api/title'))
const asyncText = useText(() => data.value ?? '', 'noto-serif')
</script>

<template>
  <h1 style="font-family: 'noto-serif', serif" v-text="text" />
  <p style="font-family: 'noto-serif', serif" v-text="asyncText" />
</template>

第三步,正常开发和构建:

bash 复制代码
pnpm dev        # 浏览器/SSR 上报文本 → 实时重建子集
pnpm build      # 静态提取声明过的字面量 → 产出子集(SPA、SSG 兼容)
pnpm generate   # prerender 收集 + 静态提取 → 产出 fonts.css + hash 命名的 woff2

就这些。不需要手动跑命令行工具,不需要维护字符清单。

背后做了什么

开发时,SSR 和浏览器会把声明过的文本通过 HTTP 上报给 dev server 的收集端点,触发子集重建。重建完成后走 HMR 热替换 fonts.css 的 link,而不是整页刷新------新 CSS 指向新 hash 的 woff2,浏览器重新下载,组件状态不丢。文本动态变化时按防抖(默认 300ms)重新收集。

构建时有两条收集通道:SSG 的 prerender 阶段在主进程内直接收集;同时一个 vite 插件会静态扫描源码,把 useText('字面量', ...) 这类可静态求值的声明直接提取进字符集------所以纯 SPA(ssr: false)做 nuxi build 也能拿到裁好的子集。产物在构建收尾时统一落盘,woff2 按内容 hash 命名,配合内容寻址的本地缓存,重复构建不会重复裁切。

效果上,以 NotoSansSC 为例:源文件约 17MB,一个常规内容量的站点裁完通常在几十 KB 量级。

边界要说清楚

这个工具有明确的适用范围,使用前最好知道:

  • 运行时才出现的动态文本不入子集。构建期能拿到的是 prerender 渲染的文本和源码里的静态字面量;接口数据、CMS 新文章这类运行时内容会缺字。兜底方式是用 include 配置把可能的字符集(如常用汉字表)注入种子。
  • 静态提取只认字面量:useText 的文本参数要写成字符串字面量(或无插值模板、字面量数组),并从 @fontize/vue 直接导入。传变量、getter 的声明在纯 SPA 的构建里提取不到(SSG 不受影响,prerender 会渲染到终态)。
  • 同一 alias 的字符集是全站页面并集,不按路由拆分。多路由共享一个子集,换页不会重新裁切。
  • 浏览器端生产环境不再收集,useText 在生产浏览器里是空操作。
  • 所以它最适合内容在构建期就确定的站点:SSG 的博客、文档站、营销页,以及大屏等以静态文本为主的纯 SPA。

最后

项目开源,仓库和在线演示(GitHub Pages):

如果你在做中文内容站,又被字体加载体积劝退过,可以试试。

相关推荐
魔力女仆4 小时前
分享一个 JS 鼠标跟随贪吃蛇背景库
开发语言·javascript·计算机外设
别惊醒渔人6 小时前
Vue3 Diff 优化:最长递增子序列 LIS
前端·javascript·vue.js
Larcher6 小时前
从“加载模型”界面到端侧推理:拆解一个 React + WebGPU 大模型 Demo
javascript·后端
Larcher7 小时前
从状态快照到惰性初始化:读懂 React useState 的三个关键场景
javascript·人工智能·后端
Hilaku8 小时前
工作 5 年后,决定你薪资上限的究竟是什么?
前端·javascript·程序员
weixin_BYSJ19879 小时前
springboot校园自习室管理小程序---附源码32142
java·javascript·spring boot·python·django·flask·php
gis开发之家10 小时前
《Vue3 从入门到大神40篇》Vue3 源码详解(十):diff 算法全解析 —— 为什么 Vue3 比 Vue2 更快?
javascript·算法·typescript·前端框架·vue3·vue3源码
用户831348593069810 小时前
Vue+Three.js实现PCB电路板3D交互:元器件点击高亮、部件显隐、模型自动旋转
vue.js·webgl·three.js
海带紫菜菠萝汤11 小时前
WebCodecs API 实战:浏览器原生视频编解码的原理与性能测试
前端·javascript·音视频·视频编解码