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