[{"data":1,"prerenderedAt":580},["ShallowReactive",2],{"post-\u002Fposts\u002Fself-hosted-fonts":3,"post-nav-\u002Fposts\u002Fself-hosted-fonts":548,"post-related-\u002Fposts\u002Fself-hosted-fonts":567},{"id":4,"title":5,"body":6,"category":532,"cover":533,"date":534,"description":535,"draft":536,"extension":537,"featured":536,"meta":538,"navigation":539,"path":540,"pinned":536,"seo":541,"stem":542,"tags":543,"updated":533,"__hash__":547},"posts\u002Fposts\u002Fself-hosted-fonts.md","自托管字体的一年",{"type":7,"value":8,"toc":523},"minimark",[9,18,21,24,28,34,37,40,62,190,196,199,206,272,275,279,304,312,318,321,336,341,344,379,382,408,411,417,420,429,432,455,465,468,481,487,490,493,496,502,505,516,519],[10,11,12,13,17],"p",{},"中文网站做自托管字体，最大的障碍不是版权，是",[14,15,16],"strong",{},"体积","。",[10,19,20],{},"一套完整的霞鹜文楷，woff2 格式大约 20-25 MB。用户打开首页就要下载这个？显然不行。",[10,22,23],{},"所以大部分中文站的选择是：用系统字体。然后全站看起来就像一张没洗过的 PPT。",[25,26,27],"h2",{"id":27},"分片是怎么解决的",[10,29,30,31],{},"关键洞察：",[14,32,33],{},"一篇文章不可能用到所有汉字。",[10,35,36],{},"常用的 3500 个汉字覆盖了 99.5% 的日常文本。剩下几万个字可能一辈子都不会出现在我的博客里。",[10,38,39],{},"所以现代中文字体包的做法是：",[41,42,43,47,55],"ol",{},[44,45,46],"li",{},"按 Unicode 范围把字体切成几百个小文件",[44,48,49,50,54],{},"每个文件用 ",[51,52,53],"code",{},"unicode-range"," 声明自己负责哪些码位",[44,56,57,58,61],{},"浏览器只下载",[14,59,60],{},"页面实际用到的","那些分片",[63,64,69],"pre",{"className":65,"code":66,"language":67,"meta":68,"style":68},"language-css shiki shiki-themes github-light github-dark","\u002F* 分片 #12 负责这些码位 *\u002F\n@font-face {\n  font-family: 'LXGW WenKai Lite';\n  font-weight: 400;\n  font-display: swap;\n  src: url('.\u002Ffiles\u002Fsubset-12.woff2') format('woff2');\n  unicode-range: U+4E00-4E1F, U+4E30-4E4F;\n}\n","css","",[51,70,71,80,91,108,121,134,165,184],{"__ignoreMap":68},[72,73,76],"span",{"class":74,"line":75},"line",1,[72,77,79],{"class":78},"sJ8bj","\u002F* 分片 #12 负责这些码位 *\u002F\n",[72,81,83,87],{"class":74,"line":82},2,[72,84,86],{"class":85},"szBVR","@font-face",[72,88,90],{"class":89},"sVt8B"," {\n",[72,92,94,98,101,105],{"class":74,"line":93},3,[72,95,97],{"class":96},"sj4cs","  font-family",[72,99,100],{"class":89},": ",[72,102,104],{"class":103},"sZZnC","'LXGW WenKai Lite'",[72,106,107],{"class":89},";\n",[72,109,111,114,116,119],{"class":74,"line":110},4,[72,112,113],{"class":96},"  font-weight",[72,115,100],{"class":89},[72,117,118],{"class":96},"400",[72,120,107],{"class":89},[72,122,124,127,129,132],{"class":74,"line":123},5,[72,125,126],{"class":96},"  font-display",[72,128,100],{"class":89},[72,130,131],{"class":96},"swap",[72,133,107],{"class":89},[72,135,137,140,142,145,148,151,154,157,159,162],{"class":74,"line":136},6,[72,138,139],{"class":96},"  src",[72,141,100],{"class":89},[72,143,144],{"class":96},"url",[72,146,147],{"class":89},"(",[72,149,150],{"class":103},"'.\u002Ffiles\u002Fsubset-12.woff2'",[72,152,153],{"class":89},") ",[72,155,156],{"class":96},"format",[72,158,147],{"class":89},[72,160,161],{"class":103},"'woff2'",[72,163,164],{"class":89},");\n",[72,166,168,171,173,176,179,182],{"class":74,"line":167},7,[72,169,170],{"class":96},"  unicode-range",[72,172,100],{"class":89},[72,174,175],{"class":96},"U+4E00-4E1F",[72,177,178],{"class":89},", ",[72,180,181],{"class":96},"U+4E30-4E4F",[72,183,107],{"class":89},[72,185,187],{"class":74,"line":186},8,[72,188,189],{"class":89},"}\n",[10,191,192,193,195],{},"浏览器渲染一段文字时，会先检查每个字符属于哪个 ",[51,194,53],{},"，只下载覆盖到的分片。",[25,197,198],{"id":198},"实际数字",[10,200,201,202,205],{},"我这个博客用 ",[51,203,204],{},"lxgw-wenkai-lite-webfont","：",[207,208,209,222],"table",{},[210,211,212],"thead",{},[213,214,215,219],"tr",{},[216,217,218],"th",{},"项目",[216,220,221],{},"数值",[223,224,225,234,242,250,260],"tbody",{},[213,226,227,231],{},[228,229,230],"td",{},"包总大小",[228,232,233],{},"25.1 MB",[213,235,236,239],{},[228,237,238],{},"分片数量",[228,240,241],{},"582 个",[213,243,244,247],{},[228,245,246],{},"单篇长文实际加载",[228,248,249],{},"3-5 个分片",[213,251,252,255],{},[228,253,254],{},"实际传输",[228,256,257],{},[14,258,259],{},"约 180 KB",[213,261,262,265],{},[228,263,264],{},"首屏延迟",[228,266,267,268,271],{},"0（",[51,269,270],{},"font-display: swap","）",[10,273,274],{},"25MB 的包，实际只用了 180KB。",[25,276,278],{"id":277},"font-display-swap-的必要性","font-display: swap 的必要性",[63,280,282],{"className":65,"code":281,"language":67,"meta":68,"style":68},"@font-face {\n  font-display: swap;\n}\n",[51,283,284,290,300],{"__ignoreMap":68},[72,285,286,288],{"class":74,"line":75},[72,287,86],{"class":85},[72,289,90],{"class":89},[72,291,292,294,296,298],{"class":74,"line":82},[72,293,126],{"class":96},[72,295,100],{"class":89},[72,297,131],{"class":96},[72,299,107],{"class":89},[72,301,302],{"class":74,"line":93},[72,303,189],{"class":89},[10,305,306,308,309],{},[51,307,131],{}," 的意思是：",[14,310,311],{},"先用后备字体渲染，字体加载完再换。",[10,313,314,315,17],{},"代价是会看到一次字体闪烁（FOIT → FOUT）。收益是",[14,316,317],{},"文字永远可读",[10,319,320],{},"另外两个选项都不适合中文：",[322,323,324,330],"ul",{},[44,325,326,329],{},[51,327,328],{},"block","：字体没加载完就不显示文字。中文 180KB 可能要 500ms，用户会看到一片空白",[44,331,332,335],{},[51,333,334],{},"optional","：网络慢就永远不用自定义字体。会导致同一篇文章有时是文楷有时是黑体",[10,337,338,340],{},[51,339,131],{}," 是唯一合理的选项。",[25,342,343],{"id":343},"后备字体链要写对",[63,345,347],{"className":65,"code":346,"language":67,"meta":68,"style":68},"font-family:\n  'Inter Variable',\n  'LXGW WenKai Lite',\n  'PingFang SC',\n  'Microsoft YaHei',\n  sans-serif;\n",[51,348,349,354,359,364,369,374],{"__ignoreMap":68},[72,350,351],{"class":74,"line":75},[72,352,353],{"class":89},"font-family:\n",[72,355,356],{"class":74,"line":82},[72,357,358],{"class":89},"  'Inter Variable',\n",[72,360,361],{"class":74,"line":93},[72,362,363],{"class":89},"  'LXGW WenKai Lite',\n",[72,365,366],{"class":74,"line":110},[72,367,368],{"class":89},"  'PingFang SC',\n",[72,370,371],{"class":74,"line":123},[72,372,373],{"class":89},"  'Microsoft YaHei',\n",[72,375,376],{"class":74,"line":136},[72,377,378],{"class":89},"  sans-serif;\n",[10,380,381],{},"这个链的顺序有讲究：",[41,383,384,390,396,402],{},[44,385,386,389],{},[14,387,388],{},"Inter"," 负责拉丁字母和数字 —— 它的西文比文楷好看得多",[44,391,392,395],{},[14,393,394],{},"文楷"," 负责汉字 —— 在 Inter 没有对应字形时接管",[44,397,398,401],{},[14,399,400],{},"苹方 \u002F 微软雅黑"," —— 文楷分片还没加载完时的后备",[44,403,404,407],{},[14,405,406],{},"sans-serif"," —— 最后兜底",[10,409,410],{},"关键在于第 1 和第 2 的顺序：浏览器是逐字符查找的。英文字符在 Inter 里能找到，就不会去问文楷；汉字在 Inter 里找不到，自然落到文楷。",[10,412,413,414,416],{},"这样西文和中文各用各的最优字体，不需要任何 ",[51,415,53],{}," 技巧。",[25,418,419],{"id":419},"等宽字体只给数据",[63,421,423],{"className":65,"code":422,"language":67,"meta":68,"style":68},"font-family: 'JetBrains Mono', 'LXGW WenKai Lite', ui-monospace, monospace;\n",[51,424,425],{"__ignoreMap":68},[72,426,427],{"class":74,"line":75},[72,428,422],{"class":89},[10,430,431],{},"等宽字体我用在四个地方，只有四个：",[322,433,434,440,446,449],{},[44,435,436,437,271],{},"日期（",[51,438,439],{},"2025-10-28",[44,441,442,443,271],{},"阅读时长（",[51,444,445],{},"6 min",[44,447,448],{},"代码块",[44,450,451,454],{},[51,452,453],{},"kbd"," 元素",[10,456,457,460,461,464],{},[14,458,459],{},"不用它做装饰性的小标签。"," 那些 ",[51,462,463],{},"MONOSPACE"," 风格的 eyebrow 文本是模板化的标志之一——它看起来\"有技术感\"，但没传达任何信息。",[25,466,467],{"id":467},"一个坑",[10,469,470,471,473,474,476,477,480],{},"字体分片依赖 ",[51,472,53],{},"，而 ",[51,475,53],{}," 是",[14,478,479],{},"静态声明","的。",[10,482,483,484,486],{},"意思是：如果你的文章里出现了某个生僻字，而那个字所在的分片没有被任何 ",[51,485,86],{}," 覆盖（有些字体包会漏掉扩展区），那个字就会掉到后备字体。",[10,488,489],{},"结果是一篇文章里 99% 是文楷，中间蹦出一个黑体的字。",[10,491,492],{},"我的处理是接受它——比为了一个字加载一整个 3MB 的扩展区划算得多。",[25,494,495],{"id":495},"结论",[497,498,499],"blockquote",{},[10,500,501],{},"中文字体不是\"能不能自托管\"的问题，是\"有没有做分片\"的问题。",[10,503,504],{},"用了分片之后：",[322,506,507,510,513],{},[44,508,509],{},"首屏字体成本 ≈ 180KB（可接受）",[44,511,512],{},"视觉一致性 100%（全站统一字体）",[44,514,515],{},"零第三方请求（隐私友好，也不受 CDN 挂掉影响）",[10,517,518],{},"这套方案我已经跑了一年，没有换回去的理由。",[520,521,522],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#57606A;--shiki-dark:#9AA4B0}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":68,"searchDepth":82,"depth":82,"links":524},[525,526,527,528,529,530,531],{"id":27,"depth":82,"text":27},{"id":198,"depth":82,"text":198},{"id":277,"depth":82,"text":278},{"id":343,"depth":82,"text":343},{"id":419,"depth":82,"text":419},{"id":467,"depth":82,"text":467},{"id":495,"depth":82,"text":495},"前端",null,"2025-08-08","从 Google Fonts 换成自托管，从自托管换成子集化分片。聊聊为什么一个中文字体能从 25MB 变成实际加载 180KB。",false,"md",{},true,"\u002Fposts\u002Fself-hosted-fonts",{"title":5,"description":535},"posts\u002Fself-hosted-fonts",[544,545,546],"字体","性能","中文","GOcoMxbNTFXrfmFfOz9FSm_xm1bcinan6CgsnG3AQ4c",{"prev":549,"next":559},{"path":550,"title":551,"description":552,"date":553,"updated":533,"tags":554,"category":532,"cover":558,"draft":536,"pinned":536,"featured":539},"\u002Fposts\u002Fview-transitions","View Transitions 不是动画库","用它做页面切换、做主题扩散、做列表到详情的共享元素过渡。它真正的价值不是\"让页面动起来\"，而是让你不必手动管理进入和离开两套状态。","2025-09-18",[555,556,557],"动画","浏览器","CSS","\u002Fimages\u002Fcovers\u002Fview-transition.png",{"path":560,"title":561,"description":562,"date":563,"updated":533,"tags":564,"category":532,"cover":533,"draft":536,"pinned":536,"featured":536},"\u002Fposts\u002Fno-loading-feel","无加载不是速度，是连续感","一个 300ms 的白屏比一个 800ms 的骨架屏更让人焦虑。这篇讲怎么用 SSG、预取和 view transition 把页面跳转做成「一瞬间的事」。","2025-05-15",[545,565,566],"体验","Nuxt",[568,570,572],{"path":560,"title":561,"description":562,"date":563,"updated":533,"tags":569,"category":532,"cover":533,"draft":536,"pinned":536,"featured":536},[545,565,566],{"path":550,"title":551,"description":552,"date":553,"updated":533,"tags":571,"category":532,"cover":558,"draft":536,"pinned":536,"featured":539},[555,556,557],{"path":573,"title":574,"description":575,"date":576,"updated":533,"tags":577,"category":532,"cover":533,"draft":536,"pinned":536,"featured":536},"\u002Fposts\u002Fcss-variables-as-api","我把 CSS 变量当成了 API","设计 token 写进配置文件，构建期编译成 CSS 变量，组件只读变量。这套做法让「改一处，全站生效」从口号变成了构建期保证。","2025-04-20",[557,578,579],"设计系统","构建",1794113901486]