perf(dashboard): subset MDI icon font and self-host Google Fonts#6532
perf(dashboard): subset MDI icon font and self-host Google Fonts#6532RC-CHN merged 5 commits intoAstrBotDevs:masterfrom
Conversation
Summary of ChangesHello, 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
Changelog
Using Gemini Code AssistThe 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
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 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
|
There was a problem hiding this comment.
Hey - I've found 2 issues, and left some high level feedback:
- In
subset-mdi-font.mjs,ICON_CLASS_PATTERNassumes a very specific.mdi-foo::before { content: "\XXXX" }format; consider loosening it (e.g. support:beforeas well as::beforeand 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>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
There was a problem hiding this comment.
Code Review
此拉取请求通过在构建时对 MDI 图标字体进行子集化处理,并本地托管 Google Fonts,显著优化了前端资源加载。新引入的构建脚本和 Vite 插件集成良好,确保了仅在生产构建时执行字体子集化,从而提高了开发效率。全面的单元测试覆盖了字体子集化脚本的关键逻辑和边缘情况,确保了其健壮性。此外,还包含了在子集化失败时的回退机制,以及移除了未使用的 remixicon 依赖,这些都体现了对代码质量和项目维护的关注。总体而言,这是一项高质量的改进,将大幅提升仪表盘的加载性能,特别是对于网络条件不佳或受限的用户。
…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
…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
Description / 描述
Dashboard 每次加载时,所有 icon 和字体都从外部网络请求(Google Fonts CDN + 全量 MDI 字体 7297 图标),若网络不可用或慢则阻塞整个前端渲染。本 PR 通过 MDI 字体子集化 + Google Fonts 本地托管消除外部字体依赖,大幅提升首屏加载速度。
Note
This is NOT a breaking change. / 这不是一个破坏性变更。
Modifications / 改动点
MDI Icon 字体子集化
mdi-*图标,通过subset-font生成子集字体vite dev时不会运行@mdi/font,确保 build 永远不会中断mdi-spin、mdi-rotate-*、mdi-flip-*、尺寸修饰符等),对上游 CSS 顺序变更具有鲁棒性vuetify.ts引用子集化 CSSGoogle Fonts 本地托管
vite-plugin-webfont-dl,构建时自动从 Google Fonts 下载字体并内联到 distindex.html中对fonts.googleapis.com的渲染阻塞外部请求清理
remixicon依赖Core Files / 核心文件
dashboard/src/assets/mdi-subset/*webfontDl()插件,仅 build 时启用Benchmark Results / 基准测试结果
实测 老前端拉取 4.9MB 网络流量,本改动后 拉取 1.5MB 。


Tip
实际网络环境中 Google Fonts CDN 请求通常增加 200-800ms 延迟,优化后完全消除该瓶颈。对于中国大陆用户(Google Fonts 被屏蔽),提升尤为显著。
Unit Tests / 单元测试
Architecture / 架构说明
Checklist / 检查清单
requirements.txt和pyproject.toml文件相应位置。(新增vite-plugin-webfont-dl和subset-font为前端 devDependency,不影响 Python 后端)Summary by Sourcery
通过在构建时对 MDI 图标字体进行子集化处理,并在本地自托管 Web 字体,优化 dashboard 前端资源管线,从而移除对外部字体的依赖并提升加载性能。
New Features:
Enhancements:
Build:
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:
Enhancements:
Build:
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:
Enhancements:
Build:
Tests:
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:
Enhancements:
Build:
Tests: