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