Skip to content

perf(dashboard): subset MDI icon font and self-host Google Fonts#6532

Merged
RC-CHN merged 5 commits intoAstrBotDevs:masterfrom
camera-2018:perf-dashboard-icon-font
Mar 18, 2026
Merged

perf(dashboard): subset MDI icon font and self-host Google Fonts#6532
RC-CHN merged 5 commits intoAstrBotDevs:masterfrom
camera-2018:perf-dashboard-icon-font

Conversation

@camera-2018
Copy link
Contributor

@camera-2018 camera-2018 commented Mar 17, 2026

Description / 描述

Dashboard 每次加载时,所有 icon 和字体都从外部网络请求(Google Fonts CDN + 全量 MDI 字体 7297 图标),若网络不可用或慢则阻塞整个前端渲染。本 PR 通过 MDI 字体子集化 + Google Fonts 本地托管消除外部字体依赖,大幅提升首屏加载速度。

Note

This is NOT a breaking change. / 这不是一个破坏性变更。

Modifications / 改动点

MDI Icon 字体子集化

  • 新增 scripts/subset-mdi-font.mjs 构建脚本,自动扫描源码中使用的 mdi-* 图标,通过 subset-font 生成子集字体
  • 7297 → 230 图标,woff2 体积从 387 KB 降至 12.7 KB96.7% 缩减
  • 通过 Vite 插件 buildStart 钩子自动执行(仅 production build),vite dev 时不会运行
  • 包含完整的错误处理和兜底机制:任何步骤失败时自动 fallback 到原始全量 @mdi/font,确保 build 永远不会中断
  • 通过减法方式从原始 CSS 提取所有工具类(mdi-spinmdi-rotate-*mdi-flip-*、尺寸修饰符等),对上游 CSS 顺序变更具有鲁棒性
  • 脚本导出 runMdiSubset() 函数,由 Vite 插件直接在进程内调用(无子进程开销)
  • 修改 vuetify.ts 引用子集化 CSS

Google Fonts 本地托管

  • 新增 vite-plugin-webfont-dl,构建时自动从 Google Fonts 下载字体并内联到 dist
  • 消除 index.html 中对 fonts.googleapis.com 的渲染阻塞外部请求

清理

  • 移除未使用的 remixicon 依赖

Core Files / 核心文件

文件 变更
dashboard/scripts/subset-mdi-font.mjs [NEW] 子集化构建脚本(含兜底逻辑 + 导出函数)
dashboard/tests/subsetMdiFont.test.mjs [NEW] 子集化脚本单元测试(17 test cases)
dashboard/src/assets/mdi-subset/* [NEW] 子集化字体 + CSS(自动生成)
dashboard/vite.config.ts 添加 mdiSubset() + webfontDl() 插件,仅 build 时启用
dashboard/src/plugins/vuetify.ts 切换 MDI import 为子集版
dashboard/package.json 脚本 + 依赖变更

Benchmark Results / 基准测试结果

以下数据基于本地 production build (pnpm build) 后测量。

指标 优化前 优化后 提升
MDI 图标数量 7,297 230 -96.8%
MDI 字体 (woff2) 387 KB 12.7 KB -96.7%
外部字体请求 7 0 -100%
dist 总字体大小 ~4.6 MB 1.7 MB -63%
dist 总大小 ~27 MB 24 MB -11%

实测 老前端拉取 4.9MB 网络流量,本改动后 拉取 1.5MB 。
image
image

Tip

实际网络环境中 Google Fonts CDN 请求通常增加 200-800ms 延迟,优化后完全消除该瓶颈。对于中国大陆用户(Google Fonts 被屏蔽),提升尤为显著。

Unit Tests / 单元测试

$ node --test tests/subsetMdiFont.test.mjs
✔ collectFiles yields files matching given extensions
✔ collectFiles recurses into subdirectories
✔ collectFiles skips node_modules directories
✔ collectFiles yields nothing for empty directory
✔ scanUsedIcons extracts mdi-* icon names from files
✔ scanUsedIcons excludes utility classes
✔ scanUsedIcons returns empty set when no icons found
✔ parseIconCodepoints parses icon definitions from CSS
✔ parseIconCodepoints handles CSS with semicolons inside braces
✔ parseIconCodepoints returns empty map for non-matching CSS
✔ resolveUsedIcons separates resolved and missing icons
✔ resolveUsedIcons returns all missing when iconMap is empty
✔ extractUtilityCss removes icon definitions and keeps utility rules
✔ extractUtilityCss returns empty string when only icon defs exist
✔ extractUtilityCss handles empty CSS input
✔ ICON_CLASS_PATTERN matches standard MDI icon definitions
✔ ICON_CLASS_PATTERN does not match non-icon classes
ℹ tests 17 | pass 17 | fail 0

Architecture / 架构说明

pnpm build
    │
    ▼
vite.config.ts (command === 'build')
    │
    ▼ import { runMdiSubset }
    │
scripts/subset-mdi-font.mjs
    │
    ├─ scanUsedIcons()       → 扫描 src/**/*.{vue,ts,js}
    ├─ parseIconCodepoints() → 解析 @mdi/font CSS
    ├─ resolveUsedIcons()    → 匹配使用的图标 → codepoints
    ├─ subset-font (WASM)    → 生成 woff2 + woff 子集字体
    ├─ extractUtilityCss()   → 提取工具类(减法方式)
    │
    ├─ 成功 → src/assets/mdi-subset/ (子集字体 + CSS)
    └─ 失败 → fallbackToFullFont() (复制原始全量字体)

Checklist / 检查清单

  • 😊 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。
  • 👀 我的更改经过了良好的测试,并已在上方提供了"验证步骤"和"运行截图"
  • 🤓 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到了 requirements.txtpyproject.toml 文件相应位置。(新增 vite-plugin-webfont-dlsubset-font 为前端 devDependency,不影响 Python 后端)
  • 😮 我的更改没有引入恶意代码。

Summary by Sourcery

通过在构建时对 MDI 图标字体进行子集化处理,并在本地自托管 Web 字体,优化 dashboard 前端资源管线,从而移除对外部字体的依赖并提升加载性能。

New Features:

  • 添加自动化构建脚本和 Vite 插件,根据源码中实际使用的图标生成子集 MDI 图标字体和相应 CSS。
  • 引入 Vite Web 字体下载插件,在构建过程中本地获取并打包 Google Fonts,消除运行时对外部字体 CDN 的请求。

Enhancements:

  • 将新的 MDI 子集化和 Web 字体下载插件接入 Vite 配置,使其作为标准 dashboard 构建流程的一部分运行。
  • 在 dashboard 中生成并使用专用的 MDI 子集字体资源包,替代完整的上游字体。
  • 从 dashboard 的包配置中移除未使用的 remixicon 依赖。

Build:

  • 新增用于字体子集化和 Web 字体下载的 devDependencies,并提供一个辅助 npm 脚本以便手动运行 MDI 图标子集化。
Original summary in English

Summary by Sourcery

Optimize the dashboard frontend asset pipeline by subsetting the MDI icon font at build time and self-hosting web fonts to remove external font dependencies and improve load performance.

New Features:

  • Add an automated build script and Vite plugin to generate a subset MDI icon font and CSS based on actually used icons in the source code.
  • Introduce a Vite webfont download plugin to fetch and bundle Google Fonts locally during the build, eliminating runtime requests to external font CDNs.

Enhancements:

  • Wire the new MDI subsetting and webfont download plugins into the Vite configuration so they run as part of the standard dashboard build.
  • Generate and use a dedicated MDI subset font asset bundle in the dashboard instead of the full upstream font.
  • Remove the unused remixicon dependency from the dashboard package configuration.

Build:

  • Add new devDependencies for font subsetting and webfont downloading and expose a helper npm script to run MDI icon subsetting manually.

Summary by Sourcery

Optimize dashboard font loading by subsetting the Material Design Icons font at build time and self-hosting web fonts to remove external font CDN dependencies and improve load performance.

New Features:

  • Introduce a build-time script and Vite plugin hook that generate a subset MDI icon font and CSS based on icons actually used in the dashboard source.

Enhancements:

  • Switch the dashboard to use the generated MDI subset assets instead of the full upstream font to reduce font payload size.
  • Remove the unused Remix Icon dependency from the dashboard package configuration.

Build:

  • Integrate the MDI subsetting and webfont download plugins into the Vite build configuration so they run only during production builds.
  • Add devDependencies for font subsetting and webfont downloading and expose an npm script to run the MDI subsetting manually.

Tests:

  • Add unit tests for the MDI subsetting script to validate file scanning, icon extraction, CSS parsing, and utility CSS handling.

Summary by Sourcery

Optimize the dashboard’s font loading by subsetting the Material Design Icons font at build time and self-hosting web fonts to remove external CDN dependencies and improve load performance.

New Features:

  • Add an automated build script and Vite plugin hook that generate a subset MDI icon font and CSS based on icons actually used in the dashboard source.
  • Introduce a Vite webfont download plugin to fetch and bundle Google Fonts locally during the production build.
  • Expose an npm script to run the MDI icon subsetting process manually.

Enhancements:

  • Switch the dashboard to use generated MDI subset assets instead of the full upstream font.
  • Remove the unused Remix Icon dependency from the dashboard package configuration.

Build:

  • Integrate the MDI subsetting and webfont download plugins into the Vite build configuration so they only run for production builds.
  • Add devDependencies for font subsetting and webfont downloading.

Tests:

  • Add unit tests for the MDI subsetting utilities covering file scanning, icon extraction, CSS parsing, and utility CSS extraction.

@auto-assign auto-assign bot requested review from Fridemn and Raven95676 March 17, 2026 15:26
@camera-2018 camera-2018 changed the title Perf dashboard icon font perf(dashboard): subset MDI icon font and self-host Google Fonts Mar 17, 2026
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces significant frontend performance optimizations for the dashboard by streamlining font loading. It achieves this through two primary mechanisms: subsetting the Material Design Icons (MDI) font to only include the glyphs actually used in the application, and by locally hosting Google Fonts during the build. These changes collectively eliminate external font dependencies, drastically reduce the overall font payload size, and improve the initial page load experience, particularly under challenging network conditions.

Highlights

  • MDI Icon Font Subsetting: Implemented a build script to automatically scan for used mdi-* icons and generate a subsetted font. This reduced the number of icons from 7,297 to 230 and the WOFF2 font size from 387 KB to 12.7 KB, a 96.7% reduction.
  • Google Fonts Local Hosting: Integrated vite-plugin-webfont-dl to download and inline Google Fonts during the build process, eliminating external requests to fonts.googleapis.com and removing a render-blocking dependency.
  • Performance Improvement: Significantly improved first-screen loading speed by removing external font dependencies and reducing font payload size, especially beneficial for users with slow networks or in regions where Google Fonts are blocked.
  • Robustness and Fallback: The MDI subsetting script includes comprehensive error handling and a fallback mechanism, ensuring that if any step fails, the original full @mdi/font is used, preventing build failures.
  • Dependency Cleanup: Removed the unused remixicon dependency from the project.
Changelog
  • dashboard/package.json
    • Added a new subset-icons npm script to manually run the MDI font subsetting process.
    • Removed the remixicon dependency.
    • Added subset-font and vite-plugin-webfont-dl as development dependencies.
  • dashboard/pnpm-lock.yaml
    • Updated the pnpm lock file to reflect the changes in dependencies, including the removal of remixicon and the addition of subset-font, vite-plugin-webfont-dl, and their transitive dependencies.
  • dashboard/scripts/subset-mdi-font.mjs
    • Added a new build script responsible for scanning source files for MDI icon usage, resolving codepoints, subsetting the MDI font, and generating a minimal CSS file with only the necessary icon classes and utility rules.
    • Implemented a fallback mechanism to use the full MDI font if subsetting fails.
  • dashboard/src/assets/mdi-subset/materialdesignicons-subset.css
    • Added a new auto-generated CSS file containing the styles for the subsetted Material Design Icons, including font-face definitions and individual icon class rules.
  • dashboard/src/plugins/vuetify.ts
    • Updated the import statement for Material Design Icons CSS to point to the newly generated subsetted CSS file.
  • dashboard/tests/subsetMdiFont.test.mjs
    • Added a new test file containing unit tests for the subset-mdi-font.mjs script, covering file collection, icon scanning, codepoint parsing, icon resolution, and utility CSS extraction.
  • dashboard/vite.config.ts
    • Integrated a custom Vite plugin (mdiSubset) to execute the MDI font subsetting script during production builds.
    • Added the webfontDl plugin to automatically download and inline Google Fonts during the build process.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • In subset-mdi-font.mjs, ICON_CLASS_PATTERN assumes a very specific .mdi-foo::before { content: "\XXXX" } format; consider loosening it (e.g. support :before as well as ::before and more flexible whitespace) to be more resilient against upstream @mdi/font CSS changes.
  • The subsetting summary currently compares the original TTF size against the subset WOFF2 size; using the original WOFF2 file (which you already have as MDI_WOFF2_PATH) for this comparison would give a more accurate like-for-like reduction metric.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `subset-mdi-font.mjs`, `ICON_CLASS_PATTERN` assumes a very specific `.mdi-foo::before { content: "\XXXX" }` format; consider loosening it (e.g. support `:before` as well as `::before` and more flexible whitespace) to be more resilient against upstream @mdi/font CSS changes.
- The subsetting summary currently compares the original TTF size against the subset WOFF2 size; using the original WOFF2 file (which you already have as `MDI_WOFF2_PATH`) for this comparison would give a more accurate like-for-like reduction metric.

## Individual Comments

### Comment 1
<location path="dashboard/scripts/subset-mdi-font.mjs" line_range="170-175" />
<code_context>
+        // Step 1: Scan source files for mdi-* icon names
+        const sourceFiles = collectFiles(SRC, [".vue", ".ts", ".js"]);
+        const usedIcons = scanUsedIcons(sourceFiles);
+        if (usedIcons.size === 0) {
+            throw new Error("No mdi-* icons found in source files. Something is wrong with scanning.");
+        }
+        console.log(`✅ Found ${usedIcons.size} unique mdi-* icons in source files`);
</code_context>
<issue_to_address>
**suggestion:** Handle the "no icons used" case without treating it as an error that forces a full-font fallback.

If the app legitimately stops using MDI icons, this `throw` will always fire and force the fallback path to keep shipping the full font, which undermines the subsetting goal.

Instead, consider treating `usedIcons.size === 0` as valid:
- Generate a minimal/empty subset font + CSS, or
- Short‑circuit (no‑op) and retain any existing subset assets.

This avoids treating “no icons” as an error while preserving the size benefits.

```suggestion
        const sourceFiles = collectFiles(SRC, [".vue", ".ts", ".js"]);
        const usedIcons = scanUsedIcons(sourceFiles);

        if (usedIcons.size === 0) {
            // Treat "no icons used" as a valid state: allow downstream logic to create
            // an effectively empty subset font/CSS instead of forcing a full-font fallback.
            console.log("ℹ️ No mdi-* icons found in source files. Generating an empty/minimal MDI subset.");
        } else {
            console.log(`✅ Found ${usedIcons.size} unique mdi-* icons in source files`);
        }
```
</issue_to_address>

### Comment 2
<location path="dashboard/scripts/subset-mdi-font.mjs" line_range="42" />
<code_context>
+// ── Helper functions ────────────────────────────────────────────────────────
+
+/** Recursively collect files with given extensions, skipping node_modules. */
+export function* collectFiles(dir, exts) {
+    for (const entry of readdirSync(dir, { withFileTypes: true })) {
+        const full = join(dir, entry.name);
</code_context>
<issue_to_address>
**issue (complexity):** Consider simplifying helpers and control flow (file collection, icon resolution, error handling, and CLI guard) to make the subsetting script easier to read without changing behavior.

You can reduce complexity without changing behavior by tightening a few boundaries and flattening control flow.

### 1. Simplify file collection (drop generator)

The generator doesn’t add much here and makes usage slightly less obvious. A simple recursive function that returns an array keeps behavior but is easier to scan:

```js
// Before
export function* collectFiles(dir, exts) {
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
    const full = join(dir, entry.name);
    if (entry.isDirectory() && entry.name !== "node_modules") {
      yield* collectFiles(full, exts);
    } else if (exts.includes(extname(entry.name))) {
      yield full;
    }
  }
}

// After
export function collectFiles(dir, exts, acc = []) {
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
    const full = join(dir, entry.name);
    if (entry.isDirectory() && entry.name !== "node_modules") {
      collectFiles(full, exts, acc);
    } else if (exts.includes(extname(entry.name))) {
      acc.push(full);
    }
  }
  return acc;
}

// Usage
const sourceFiles = collectFiles(SRC, [".vue", ".ts", ".js"]);
```

### 2. Inline `resolveUsedIcons` into the main pipeline

`resolveUsedIcons` is tightly coupled to logging and `subsetChars` and used only once. Inlining it makes the main flow self-contained and removes one indirection:

```js
// Before
const { resolvedIcons, missingIcons, subsetChars } = resolveUsedIcons(usedIcons, iconMap);

// After (inside runMdiSubset)
const resolvedIcons = [];
const missingIcons = [];
const subsetChars = [];

for (const icon of usedIcons) {
  const cp = iconMap.get(icon);
  if (cp) {
    resolvedIcons.push(icon);
    subsetChars.push(String.fromCodePoint(parseInt(cp, 16)));
  } else {
    missingIcons.push(icon);
  }
}
```

You can then delete the standalone `resolveUsedIcons` helper.

### 3. Separate core logic from error-handling / fallback

Right now `runMdiSubset` mixes the core pipeline, logging, and fallback. You can extract the core steps into a pure function that throws, and keep `runMdiSubset` as the wrapper that handles fallback. This keeps all current behavior but flattens the control flow:

```js
async function generateMdiSubset() {
  // everything currently inside the try { ... }
  // except the outer try/catch and fallbackToFullFont call
}

export async function runMdiSubset() {
  mkdirSync(OUT_DIR, { recursive: true });

  try {
    await generateMdiSubset();
  } catch (err) {
    try {
      fallbackToFullFont(err.message);
    } catch (fallbackErr) {
      console.error(`❌ Fallback also failed: ${fallbackErr.message}`);
      console.error(`❌ Please ensure @mdi/font is installed: pnpm install`);
      throw fallbackErr;
    }
  }
}
```

This makes the main pipeline (`generateMdiSubset`) much easier to read and test while preserving the same fallback semantics.

### 4. Simplify CLI auto-detection

The `import.meta.url.startsWith('file:')` guard is defensive but not really needed for a project-local script. Comparing `argv[1]` to `fileURLToPath(import.meta.url)` is already the standard ESM pattern and keeps behavior equivalent in practice:

```js
// Before
if (import.meta.url.startsWith('file:') && process.argv[1] === fileURLToPath(import.meta.url)) {
  runMdiSubset().catch(err => {
    console.error(err);
    process.exit(1);
  });
}

// After
if (process.argv[1] === fileURLToPath(import.meta.url)) {
  runMdiSubset().catch(err => {
    console.error(err);
    process.exit(1);
  });
}
```

These tweaks keep the subsetting + fallback functionality intact but reduce indirection and make the script easier to follow.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

此拉取请求通过在构建时对 MDI 图标字体进行子集化处理,并本地托管 Google Fonts,显著优化了前端资源加载。新引入的构建脚本和 Vite 插件集成良好,确保了仅在生产构建时执行字体子集化,从而提高了开发效率。全面的单元测试覆盖了字体子集化脚本的关键逻辑和边缘情况,确保了其健壮性。此外,还包含了在子集化失败时的回退机制,以及移除了未使用的 remixicon 依赖,这些都体现了对代码质量和项目维护的关注。总体而言,这是一项高质量的改进,将大幅提升仪表盘的加载性能,特别是对于网络条件不佳或受限的用户。

Copy link
Member

@RC-CHN RC-CHN left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mua

@dosubot dosubot bot added the lgtm This PR has been approved by a maintainer label Mar 18, 2026
@RC-CHN RC-CHN merged commit 4e55879 into AstrBotDevs:master Mar 18, 2026
6 checks passed
KBVsent pushed a commit to KBVsent/AstrBot that referenced this pull request Mar 19, 2026
…rBotDevs#6532)

* perf(dashboard): subset MDI icon font and self-host Google Fonts

* perf(dashboard): subset MDI icon font and self-host Google Fonts

* perf(dashboard): subset MDI icon font and self-host Google Fonts

* perf(dashboard): subset MDI icon font cr fix

* chore: update lockfile
KBVsent pushed a commit to KBVsent/AstrBot that referenced this pull request Mar 21, 2026
…rBotDevs#6532)

* perf(dashboard): subset MDI icon font and self-host Google Fonts

* perf(dashboard): subset MDI icon font and self-host Google Fonts

* perf(dashboard): subset MDI icon font and self-host Google Fonts

* perf(dashboard): subset MDI icon font cr fix

* chore: update lockfile
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lgtm This PR has been approved by a maintainer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants