diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 74040bbb..76344459 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -8,14 +8,10 @@ on:
- cron: '0 1 * * *' # UTC 01:00 = 北京时间 09:00
workflow_dispatch:
inputs:
- force_font_subset:
- description: 'Force font subset regeneration'
- type: boolean
- default: false
skip_font_subset:
description: 'Skip font subset step'
type: boolean
- default: false
+ default: true
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -101,7 +97,7 @@ jobs:
if: |
needs.pre-check.outputs.can_deploy == 'true' &&
inputs.skip_font_subset != 'true' &&
- (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
+ github.event_name == 'workflow_dispatch'
outputs:
font_updated: ${{ steps.update.outputs.font_updated }}
char_count: ${{ steps.stats.outputs.char_count }}
diff --git a/output.log b/output.log
new file mode 100644
index 00000000..e69de29b
diff --git a/scripts/slice_font.py b/scripts/slice_font.py
new file mode 100644
index 00000000..1d6e6169
--- /dev/null
+++ b/scripts/slice_font.py
@@ -0,0 +1,206 @@
+#!/usr/bin/env python3
+"""
+字体分片工具
+将中文字体按 Unicode 区块分成多个小文件,配合 CSS unicode-range 使用
+浏览器按需加载,大幅提升首屏加载速度
+"""
+
+import os
+import re
+from fontTools.ttLib import TTFont
+from fontTools.subset import Subsetter, Options
+
+FONT_SRC = 'themes/Ying/static/font/zql-v2.woff2'
+FONT_OUT_DIR = 'themes/Ying/static/font/slices'
+CSS_OUT_PATH = 'themes/Ying/assets/css/font-slices.css'
+
+CJK_UNIFIED_START = 0x4E00
+CJK_UNIFIED_END = 0x9FFF
+
+def generate_slices(num_slices=15):
+ """
+ 将 CJK 统一汉字区分成多个分片
+ 常用字区域(开头)分得细一些,生僻字区域分得粗一些
+ """
+ total_chars = CJK_UNIFIED_END - CJK_UNIFIED_START + 1
+
+ slices = []
+
+ # 策略:前30%常用区分得细,后70%生僻字区分得粗
+ common_ratio = 0.3
+ common_end = int(CJK_UNIFIED_START + total_chars * common_ratio)
+
+ common_slices = int(num_slices * 0.6) # 60%的分片给常用字
+ rare_slices = num_slices - common_slices
+
+ # 常用字区分片
+ common_range = common_end - CJK_UNIFIED_START
+ common_per_slice = common_range // common_slices
+
+ for i in range(common_slices):
+ start = CJK_UNIFIED_START + i * common_per_slice
+ end = CJK_UNIFIED_START + (i + 1) * common_per_slice - 1
+ if i == common_slices - 1:
+ end = common_end
+ slices.append((start, end, f'common-{i+1:02d}'))
+
+ # 生僻字区分片
+ rare_range = CJK_UNIFIED_END - common_end
+ rare_per_slice = rare_range // rare_slices
+
+ for i in range(rare_slices):
+ start = common_end + 1 + i * rare_per_slice
+ end = common_end + (i + 1) * rare_per_slice
+ if i == rare_slices - 1:
+ end = CJK_UNIFIED_END
+ slices.append((start, end, f'rare-{i+1:02d}'))
+
+ return slices
+
+def get_all_chars(font):
+ """获取字体中所有可用的字符"""
+ chars = set()
+ cmap = font.getBestCmap()
+ for codepoint in cmap:
+ if CJK_UNIFIED_START <= codepoint <= CJK_UNIFIED_END:
+ chars.add(codepoint)
+ return chars
+
+def subset_font(font_path, output_path, codepoints):
+ """生成子集字体"""
+ font = TTFont(font_path)
+
+ options = Options()
+ options.flavor = 'woff2'
+ options.desubroutinize = True
+
+ subsetter = Subsetter(options=options)
+ subsetter.populate(unicodes=codepoints)
+ subsetter.subset(font)
+
+ font.save(output_path)
+ font.close()
+
+def generate_css(slices_info, font_dir_rel='../font/slices'):
+ """生成 CSS 文件"""
+ css_parts = []
+
+ # ASCII + 标点符号(第一优先级,总是加载)
+ css_parts.append("""/**
+ * 字体分片 - 按需加载
+ * 浏览器会根据页面实际使用的字符自动加载对应的分片
+ */
+@font-face {
+ font-family: 'zql';
+ src: url('../font/zql-v2.woff2') format('woff2');
+ font-display: swap;
+ unicode-range: U+0000-00FF, U+2000-206F, U+3000-303F, U+FF00-FFEF;
+}
+""")
+
+ for name, codepoints, size in slices_info:
+ if not codepoints:
+ continue
+
+ # 生成 unicode-range
+ ranges = []
+ sorted_points = sorted(codepoints)
+ range_start = sorted_points[0]
+ prev = sorted_points[0]
+
+ for cp in sorted_points[1:]:
+ if cp == prev + 1:
+ prev = cp
+ else:
+ if range_start == prev:
+ ranges.append(f'U+{range_start:04X}')
+ else:
+ ranges.append(f'U+{range_start:04X}-{prev:04X}')
+ range_start = cp
+ prev = cp
+
+ if range_start == prev:
+ ranges.append(f'U+{range_start:04X}')
+ else:
+ ranges.append(f'U+{range_start:04X}-{prev:04X}')
+
+ unicode_range = ', '.join(ranges)
+
+ css_parts.append(f"""@font-face {{
+ font-family: 'zql';
+ src: url('{font_dir_rel}/zql-{name}.woff2') format('woff2');
+ font-display: swap;
+ unicode-range: {unicode_range};
+}}
+""")
+
+ return '\n'.join(css_parts)
+
+def main():
+ print('🚀 开始字体分片...')
+ print(f'📄 源字体: {FONT_SRC}')
+
+ if not os.path.exists(FONT_SRC):
+ print(f'❌ 源字体不存在: {FONT_SRC}')
+ return
+
+ os.makedirs(FONT_OUT_DIR, exist_ok=True)
+
+ # 读取源字体,获取所有字符
+ print('🔍 读取源字体...')
+ font = TTFont(FONT_SRC)
+ all_chars = get_all_chars(font)
+ font.close()
+ print(f'✅ 共 {len(all_chars)} 个 CJK 字符')
+
+ # 生成分片方案
+ slices = generate_slices(num_slices=12)
+ print(f'📦 分成 {len(slices)} 个分片')
+
+ # 处理每个分片
+ slices_info = []
+
+ for start, end, name in slices:
+ # 获取这个区间内字体实际包含的字符
+ slice_chars = [cp for cp in all_chars if start <= cp <= end]
+
+ if not slice_chars:
+ print(f' ⏭️ 分片 {name} (U+{start:04X}-U+{end:04X}): 无字符,跳过')
+ slices_info.append((name, [], 0))
+ continue
+
+ output_path = os.path.join(FONT_OUT_DIR, f'zql-{name}.woff2')
+
+ print(f' ✂️ 分片 {name} (U+{start:04X}-U+{end:04X}): {len(slice_chars)} 字')
+ subset_font(FONT_SRC, output_path, slice_chars)
+
+ size = os.path.getsize(output_path)
+ slices_info.append((name, slice_chars, size))
+ print(f' → {size/1024:.1f} KB')
+
+ # 生成 CSS
+ print('\n📝 生成 CSS 文件...')
+ css = generate_css(slices_info)
+
+ with open(CSS_OUT_PATH, 'w', encoding='utf-8') as f:
+ f.write(css)
+
+ print(f'✅ CSS 已保存: {CSS_OUT_PATH}')
+
+ # 统计
+ total_size = sum(size for _, _, size in slices_info)
+ original_size = os.path.getsize(FONT_SRC)
+
+ print('\n' + '='*50)
+ print('📊 统计信息')
+ print('='*50)
+ print(f'原始字体大小: {original_size/1024:.1f} KB')
+ print(f'所有分片总大小: {total_size/1024:.1f} KB')
+ print(f'分片数量: {len(slices_info)}')
+ print()
+ print('预估首屏加载: 50-150 KB (常用字 3-5 个分片)')
+ print('='*50)
+ print('✅ 字体分片完成!')
+
+if __name__ == '__main__':
+ main()
diff --git a/themes/Ying/assets/css/font-slices.css b/themes/Ying/assets/css/font-slices.css
new file mode 100644
index 00000000..0d66b4c9
--- /dev/null
+++ b/themes/Ying/assets/css/font-slices.css
@@ -0,0 +1,85 @@
+/**
+ * 字体分片 - 按需加载
+ * 浏览器会根据页面实际使用的字符自动加载对应的分片
+ * 首屏通常只需要加载 ASCII + 1-3 个常用字分片(约 100-200KB)
+ */
+
+/* ASCII + 常用标点(基础,总是加载) */
+@font-face {
+ font-family: 'zql';
+ src: url('../font/zql-v2.woff2') format('woff2');
+ font-display: swap;
+ unicode-range: U+0000-00FF, U+2000-206F, U+3000-303F, U+FF00-FFEF;
+}
+
+/* 常用字分片 01: U+4E00-U+56FF */
+@font-face {
+ font-family: 'zql';
+ src: url('../font/slices/zql-common-01.woff2') format('woff2');
+ font-display: swap;
+ unicode-range: U+4E00-56FF;
+}
+
+/* 常用字分片 02: U+5700-U+5FFF */
+@font-face {
+ font-family: 'zql';
+ src: url('../font/slices/zql-common-02.woff2') format('woff2');
+ font-display: swap;
+ unicode-range: U+5700-5FFF;
+}
+
+/* 常用字分片 03: U+6000-U+68FF */
+@font-face {
+ font-family: 'zql';
+ src: url('../font/slices/zql-common-03.woff2') format('woff2');
+ font-display: swap;
+ unicode-range: U+6000-68FF;
+}
+
+/* 常用字分片 04: U+6900-U+71FF */
+@font-face {
+ font-family: 'zql';
+ src: url('../font/slices/zql-common-04.woff2') format('woff2');
+ font-display: swap;
+ unicode-range: U+6900-71FF;
+}
+
+/* 常用字分片 05: U+7200-U+7AFF */
+@font-face {
+ font-family: 'zql';
+ src: url('../font/slices/zql-common-05.woff2') format('woff2');
+ font-display: swap;
+ unicode-range: U+7200-7AFF;
+}
+
+/* 常用字分片 06: U+7B00-U+83FF */
+@font-face {
+ font-family: 'zql';
+ src: url('../font/slices/zql-common-06.woff2') format('woff2');
+ font-display: swap;
+ unicode-range: U+7B00-83FF;
+}
+
+/* 常用字分片 07: U+8400-U+8CFF */
+@font-face {
+ font-family: 'zql';
+ src: url('../font/slices/zql-common-07.woff2') format('woff2');
+ font-display: swap;
+ unicode-range: U+8400-8CFF;
+}
+
+/* 生僻字分片 01: U+8D00-U+95FF */
+@font-face {
+ font-family: 'zql';
+ src: url('../font/slices/zql-rare-01.woff2') format('woff2');
+ font-display: swap;
+ unicode-range: U+8D00-95FF;
+}
+
+/* 生僻字分片 02: U+9600-U+9FFF */
+@font-face {
+ font-family: 'zql';
+ src: url('../font/slices/zql-rare-02.woff2') format('woff2');
+ font-display: swap;
+ unicode-range: U+9600-9FFF;
+}
diff --git a/themes/Ying/assets/css/main.css b/themes/Ying/assets/css/main.css
index bad0403e..e780c329 100644
--- a/themes/Ying/assets/css/main.css
+++ b/themes/Ying/assets/css/main.css
@@ -1,14 +1,3 @@
-@font-face {
- font-family: 'zql';
- src: url('../font/zql-v2-subset.woff2') format('woff2'),
- url('../font/zql-v2-subset.woff') format('woff');
- font-display: swap;
-}
-
-
-
-
-
@charset "UTF-8";:root {
--background-color: #fafafa;
--background-dark-color: #020617;
diff --git a/themes/Ying/layouts/partials/head.html b/themes/Ying/layouts/partials/head.html
index c1a94f61..09f8b3c0 100644
--- a/themes/Ying/layouts/partials/head.html
+++ b/themes/Ying/layouts/partials/head.html
@@ -75,7 +75,8 @@
})();
-
+
+
{{ $echartsHead := resources.Get "js/libs/echarts.min.js" | resources.Fingerprint }}
@@ -130,10 +131,11 @@
{{ $uikit := resources.Get "css/libs/uikit.min.css" }}
{{ $nprogress := resources.Get "css/libs/nprogress.min.css" }}
{{ $message := resources.Get "css/libs/message.min.css" }}
+ {{ $fontSlices := resources.Get "css/font-slices.css" }}
{{ $main := resources.Get "css/main.css" }}
{{ $shortcodes := resources.Get "css/shortcodes.css" }}
- {{ $style := slice $normalize $remixicon $uikit $nprogress $message $main $shortcodes | resources.Concat "css/style.css" | resources.Minify | resources.Fingerprint }}
+ {{ $style := slice $normalize $remixicon $uikit $nprogress $message $fontSlices $main $shortcodes | resources.Concat "css/style.css" | resources.Minify | resources.Fingerprint }}
{{- if and .Site.Params.style.enableBackgroundImage .Site.Params.style.backgroundImageUrl -}}
diff --git a/themes/Ying/static/font/slices/zql-common-01.woff2 b/themes/Ying/static/font/slices/zql-common-01.woff2
new file mode 100644
index 00000000..e0a5ebe8
Binary files /dev/null and b/themes/Ying/static/font/slices/zql-common-01.woff2 differ
diff --git a/themes/Ying/static/font/slices/zql-common-02.woff2 b/themes/Ying/static/font/slices/zql-common-02.woff2
new file mode 100644
index 00000000..4d957ef7
Binary files /dev/null and b/themes/Ying/static/font/slices/zql-common-02.woff2 differ
diff --git a/themes/Ying/static/font/slices/zql-common-03.woff2 b/themes/Ying/static/font/slices/zql-common-03.woff2
new file mode 100644
index 00000000..822cf64d
Binary files /dev/null and b/themes/Ying/static/font/slices/zql-common-03.woff2 differ
diff --git a/themes/Ying/static/font/slices/zql-common-04.woff2 b/themes/Ying/static/font/slices/zql-common-04.woff2
new file mode 100644
index 00000000..8413a09f
Binary files /dev/null and b/themes/Ying/static/font/slices/zql-common-04.woff2 differ
diff --git a/themes/Ying/static/font/slices/zql-common-05.woff2 b/themes/Ying/static/font/slices/zql-common-05.woff2
new file mode 100644
index 00000000..b84bf11f
Binary files /dev/null and b/themes/Ying/static/font/slices/zql-common-05.woff2 differ
diff --git a/themes/Ying/static/font/slices/zql-common-06.woff2 b/themes/Ying/static/font/slices/zql-common-06.woff2
new file mode 100644
index 00000000..6e325853
Binary files /dev/null and b/themes/Ying/static/font/slices/zql-common-06.woff2 differ
diff --git a/themes/Ying/static/font/slices/zql-common-07.woff2 b/themes/Ying/static/font/slices/zql-common-07.woff2
new file mode 100644
index 00000000..9c271ee0
Binary files /dev/null and b/themes/Ying/static/font/slices/zql-common-07.woff2 differ
diff --git a/themes/Ying/static/font/slices/zql-rare-01.woff2 b/themes/Ying/static/font/slices/zql-rare-01.woff2
new file mode 100644
index 00000000..658d5147
Binary files /dev/null and b/themes/Ying/static/font/slices/zql-rare-01.woff2 differ
diff --git a/themes/Ying/static/font/slices/zql-rare-02.woff2 b/themes/Ying/static/font/slices/zql-rare-02.woff2
new file mode 100644
index 00000000..ffe2979a
Binary files /dev/null and b/themes/Ying/static/font/slices/zql-rare-02.woff2 differ