[{"data":1,"prerenderedAt":997},["ShallowReactive",2],{"post-\u002Fposts\u002F7e4167d8":3,"post-nav-\u002Fposts\u002F7e4167d8":959,"post-related-\u002Fposts\u002F7e4167d8":978},{"id":4,"title":5,"abbrlink":6,"aisummary":7,"body":8,"category":943,"cover":944,"date":945,"description":946,"draft":947,"extension":948,"featured":188,"meta":949,"navigation":188,"path":950,"pinned":947,"seo":951,"stem":952,"tags":953,"updated":957,"__hash__":958},"posts\u002Fposts\u002Fview-transitions.md","View Transitions 不是动画库","7e4167d8","View Transitions 是一个用于页面状态切换的 API，而非动画库。它通过浏览器自动处理旧状态和新状态的截图与过渡，简化了传统页面转场中需手动管理的六个步骤。支持页面切换、主题切换和共享元素动画，但需注意同一时间只能有一个过渡名称、动画期间页面不可交互以及需尊重用户减少运动的偏好。适合用于页面级状态变化，而非复杂动画或微交互。",{"type":9,"value":10,"toc":932},"minimark",[11,20,23,27,35,38,60,63,66,120,123,128,131,136,143,331,334,338,349,629,639,643,650,678,681,756,759,767,773,778,781,789,878,881,884,909,916,919,925,928],[12,13,14,15,19],"p",{},"第一次看到 ",[16,17,18],"code",{},"document.startViewTransition"," 的时候，我以为它就是个更简单的 GSAP。",[12,21,22],{},"用了半年之后我发现，它真正解决的问题根本不是\"怎么动画\"。",[24,25,26],"h2",{"id":26},"它解决的是什么问题",[12,28,29,30,34],{},"传统的页面转场动画难写，不是因为 CSS 难，而是因为",[31,32,33],"strong",{},"你要同时管理两个状态","。",[12,36,37],{},"从列表页跳到详情页，你得：",[39,40,41,45,48,51,54,57],"ol",{},[42,43,44],"li",{},"给旧页面加一个 \"leaving\" 类",[42,46,47],{},"等动画结束",[42,49,50],{},"卸载旧页面",[42,52,53],{},"挂载新页面",[42,55,56],{},"给新页面加一个 \"entering\" 类",[42,58,59],{},"再等动画结束",[12,61,62],{},"六个步骤，任何一步时序错一点就会闪。",[12,64,65],{},"View Transitions 把这六步变成了一个 API：",[67,68,73],"pre",{"className":69,"code":70,"language":71,"meta":72,"style":72},"language-ts shiki shiki-themes github-light github-dark","document.startViewTransition(() => {\n  \u002F\u002F 只在这里改 DOM，浏览器负责前后的快照和过渡\n  renderNewPage()\n})\n","ts","",[16,74,75,98,105,114],{"__ignoreMap":72},[76,77,80,84,88,91,95],"span",{"class":78,"line":79},"line",1,[76,81,83],{"class":82},"sVt8B","document.",[76,85,87],{"class":86},"sScJk","startViewTransition",[76,89,90],{"class":82},"(() ",[76,92,94],{"class":93},"szBVR","=>",[76,96,97],{"class":82}," {\n",[76,99,101],{"class":78,"line":100},2,[76,102,104],{"class":103},"sJ8bj","  \u002F\u002F 只在这里改 DOM，浏览器负责前后的快照和过渡\n",[76,106,108,111],{"class":78,"line":107},3,[76,109,110],{"class":86},"  renderNewPage",[76,112,113],{"class":82},"()\n",[76,115,117],{"class":78,"line":116},4,[76,118,119],{"class":82},"})\n",[12,121,122],{},"浏览器会：对旧状态截图 → 执行你的回调 → 对新状态截图 → 在两帧之间做过渡。",[12,124,125],{},[31,126,127],{},"你只描述\"变成什么样\"，不描述\"怎么变\"。",[24,129,130],{"id":130},"三种实际用法",[132,133,135],"h3",{"id":134},"一页面切换","一、页面切换",[12,137,138,139,142],{},"Nuxt 开了 ",[16,140,141],{},"experimental.viewTransition"," 之后，页面过渡可以完全用 CSS 控制：",[67,144,148],{"className":145,"code":146,"language":147,"meta":72,"style":72},"language-css shiki shiki-themes github-light github-dark","::view-transition-old(root) {\n  animation: 200ms ease-out both fade-out;\n}\n\n::view-transition-new(root) {\n  animation: 200ms ease-out both fade-in;\n}\n\n@keyframes fade-out {\n  to { opacity: 0; }\n}\n\n@keyframes fade-in {\n  from {\n    opacity: 0;\n    transform: translateY(4px);\n  }\n}\n","css",[16,149,150,155,179,184,190,196,214,219,224,236,256,261,266,276,284,297,320,326],{"__ignoreMap":72},[76,151,152],{"class":78,"line":79},[76,153,154],{"class":82},"::view-transition-old(root) {\n",[76,156,157,161,164,167,170,173,176],{"class":78,"line":100},[76,158,160],{"class":159},"sj4cs","  animation",[76,162,163],{"class":82},": ",[76,165,166],{"class":159},"200",[76,168,169],{"class":93},"ms",[76,171,172],{"class":159}," ease-out",[76,174,175],{"class":159}," both",[76,177,178],{"class":82}," fade-out;\n",[76,180,181],{"class":78,"line":107},[76,182,183],{"class":82},"}\n",[76,185,186],{"class":78,"line":116},[76,187,189],{"emptyLinePlaceholder":188},true,"\n",[76,191,193],{"class":78,"line":192},5,[76,194,195],{"class":82},"::view-transition-new(root) {\n",[76,197,199,201,203,205,207,209,211],{"class":78,"line":198},6,[76,200,160],{"class":159},[76,202,163],{"class":82},[76,204,166],{"class":159},[76,206,169],{"class":93},[76,208,172],{"class":159},[76,210,175],{"class":159},[76,212,213],{"class":82}," fade-in;\n",[76,215,217],{"class":78,"line":216},7,[76,218,183],{"class":82},[76,220,222],{"class":78,"line":221},8,[76,223,189],{"emptyLinePlaceholder":188},[76,225,227,230,234],{"class":78,"line":226},9,[76,228,229],{"class":93},"@keyframes",[76,231,233],{"class":232},"s4XuR"," fade-out",[76,235,97],{"class":82},[76,237,239,242,245,248,250,253],{"class":78,"line":238},10,[76,240,241],{"class":86},"  to",[76,243,244],{"class":82}," { ",[76,246,247],{"class":159},"opacity",[76,249,163],{"class":82},[76,251,252],{"class":159},"0",[76,254,255],{"class":82},"; }\n",[76,257,259],{"class":78,"line":258},11,[76,260,183],{"class":82},[76,262,264],{"class":78,"line":263},12,[76,265,189],{"emptyLinePlaceholder":188},[76,267,269,271,274],{"class":78,"line":268},13,[76,270,229],{"class":93},[76,272,273],{"class":232}," fade-in",[76,275,97],{"class":82},[76,277,279,282],{"class":78,"line":278},14,[76,280,281],{"class":86},"  from",[76,283,97],{"class":82},[76,285,287,290,292,294],{"class":78,"line":286},15,[76,288,289],{"class":159},"    opacity",[76,291,163],{"class":82},[76,293,252],{"class":159},[76,295,296],{"class":82},";\n",[76,298,300,303,305,308,311,314,317],{"class":78,"line":299},16,[76,301,302],{"class":159},"    transform",[76,304,163],{"class":82},[76,306,307],{"class":159},"translateY",[76,309,310],{"class":82},"(",[76,312,313],{"class":159},"4",[76,315,316],{"class":93},"px",[76,318,319],{"class":82},");\n",[76,321,323],{"class":78,"line":322},17,[76,324,325],{"class":82},"  }\n",[76,327,329],{"class":78,"line":328},18,[76,330,183],{"class":82},[12,332,333],{},"注意这里没有 JS。转场逻辑全在 CSS 里，JS 只负责换内容。",[132,335,337],{"id":336},"二主题切换的圆形扩散","二、主题切换的圆形扩散",[12,339,340,341,344,345,348],{},"这个效果很多人以为是手写 ",[16,342,343],{},"clip-path"," 加 ",[16,346,347],{},"requestAnimationFrame","，其实可以完全交给 View Transitions：",[67,350,352],{"className":69,"code":351,"language":71,"meta":72,"style":72},"async function toggleTheme(x: number, y: number) {\n  const radius = Math.hypot(\n    Math.max(x, innerWidth - x),\n    Math.max(y, innerHeight - y),\n  )\n\n  const transition = document.startViewTransition(() => {\n    document.documentElement.classList.toggle('dark')\n  })\n\n  await transition.ready\n\n  document.documentElement.animate(\n    {\n      clipPath: [\n        `circle(0px at ${x}px ${y}px)`,\n        `circle(${radius}px at ${x}px ${y}px)`,\n      ],\n    },\n    {\n      duration: 200,\n      easing: 'ease-out',\n      pseudoElement: '::view-transition-new(root)',\n    },\n  )\n}\n",[16,353,354,389,409,426,440,445,449,469,486,491,495,503,507,517,522,527,545,566,571,577,582,592,603,614,619,624],{"__ignoreMap":72},[76,355,356,359,362,365,367,370,373,376,379,382,384,386],{"class":78,"line":79},[76,357,358],{"class":93},"async",[76,360,361],{"class":93}," function",[76,363,364],{"class":86}," toggleTheme",[76,366,310],{"class":82},[76,368,369],{"class":232},"x",[76,371,372],{"class":93},":",[76,374,375],{"class":159}," number",[76,377,378],{"class":82},", ",[76,380,381],{"class":232},"y",[76,383,372],{"class":93},[76,385,375],{"class":159},[76,387,388],{"class":82},") {\n",[76,390,391,394,397,400,403,406],{"class":78,"line":100},[76,392,393],{"class":93},"  const",[76,395,396],{"class":159}," radius",[76,398,399],{"class":93}," =",[76,401,402],{"class":82}," Math.",[76,404,405],{"class":86},"hypot",[76,407,408],{"class":82},"(\n",[76,410,411,414,417,420,423],{"class":78,"line":107},[76,412,413],{"class":82},"    Math.",[76,415,416],{"class":86},"max",[76,418,419],{"class":82},"(x, innerWidth ",[76,421,422],{"class":93},"-",[76,424,425],{"class":82}," x),\n",[76,427,428,430,432,435,437],{"class":78,"line":116},[76,429,413],{"class":82},[76,431,416],{"class":86},[76,433,434],{"class":82},"(y, innerHeight ",[76,436,422],{"class":93},[76,438,439],{"class":82}," y),\n",[76,441,442],{"class":78,"line":192},[76,443,444],{"class":82},"  )\n",[76,446,447],{"class":78,"line":198},[76,448,189],{"emptyLinePlaceholder":188},[76,450,451,453,456,458,461,463,465,467],{"class":78,"line":216},[76,452,393],{"class":93},[76,454,455],{"class":159}," transition",[76,457,399],{"class":93},[76,459,460],{"class":82}," document.",[76,462,87],{"class":86},[76,464,90],{"class":82},[76,466,94],{"class":93},[76,468,97],{"class":82},[76,470,471,474,477,479,483],{"class":78,"line":221},[76,472,473],{"class":82},"    document.documentElement.classList.",[76,475,476],{"class":86},"toggle",[76,478,310],{"class":82},[76,480,482],{"class":481},"sZZnC","'dark'",[76,484,485],{"class":82},")\n",[76,487,488],{"class":78,"line":226},[76,489,490],{"class":82},"  })\n",[76,492,493],{"class":78,"line":238},[76,494,189],{"emptyLinePlaceholder":188},[76,496,497,500],{"class":78,"line":258},[76,498,499],{"class":93},"  await",[76,501,502],{"class":82}," transition.ready\n",[76,504,505],{"class":78,"line":263},[76,506,189],{"emptyLinePlaceholder":188},[76,508,509,512,515],{"class":78,"line":268},[76,510,511],{"class":82},"  document.documentElement.",[76,513,514],{"class":86},"animate",[76,516,408],{"class":82},[76,518,519],{"class":78,"line":278},[76,520,521],{"class":82},"    {\n",[76,523,524],{"class":78,"line":286},[76,525,526],{"class":82},"      clipPath: [\n",[76,528,529,532,534,537,539,542],{"class":78,"line":299},[76,530,531],{"class":481},"        `circle(0px at ${",[76,533,369],{"class":82},[76,535,536],{"class":481},"}px ${",[76,538,381],{"class":82},[76,540,541],{"class":481},"}px)`",[76,543,544],{"class":82},",\n",[76,546,547,550,553,556,558,560,562,564],{"class":78,"line":322},[76,548,549],{"class":481},"        `circle(${",[76,551,552],{"class":82},"radius",[76,554,555],{"class":481},"}px at ${",[76,557,369],{"class":82},[76,559,536],{"class":481},[76,561,381],{"class":82},[76,563,541],{"class":481},[76,565,544],{"class":82},[76,567,568],{"class":78,"line":328},[76,569,570],{"class":82},"      ],\n",[76,572,574],{"class":78,"line":573},19,[76,575,576],{"class":82},"    },\n",[76,578,580],{"class":78,"line":579},20,[76,581,521],{"class":82},[76,583,585,588,590],{"class":78,"line":584},21,[76,586,587],{"class":82},"      duration: ",[76,589,166],{"class":159},[76,591,544],{"class":82},[76,593,595,598,601],{"class":78,"line":594},22,[76,596,597],{"class":82},"      easing: ",[76,599,600],{"class":481},"'ease-out'",[76,602,544],{"class":82},[76,604,606,609,612],{"class":78,"line":605},23,[76,607,608],{"class":82},"      pseudoElement: ",[76,610,611],{"class":481},"'::view-transition-new(root)'",[76,613,544],{"class":82},[76,615,617],{"class":78,"line":616},24,[76,618,576],{"class":82},[76,620,622],{"class":78,"line":621},25,[76,623,444],{"class":82},[76,625,627],{"class":78,"line":626},26,[76,628,183],{"class":82},[12,630,631,632,635,636,34],{},"关键在 ",[16,633,634],{},"pseudoElement: '::view-transition-new(root)'","——你不是在动画真实 DOM，是在动画浏览器的过渡快照层。所以这个动画",[31,637,638],{},"不会触发重排",[132,640,642],{"id":641},"三列表到详情的共享元素","三、列表到详情的共享元素",[12,644,645,646,649],{},"给元素一个 ",[16,647,648],{},"view-transition-name","：",[67,651,653],{"className":145,"code":652,"language":147,"meta":72,"style":72},".post-card__title { view-transition-name: post-title; }\n.post-detail__title { view-transition-name: post-title; }\n",[16,654,655,667],{"__ignoreMap":72},[76,656,657,660,662,664],{"class":78,"line":79},[76,658,659],{"class":86},".post-card__title",[76,661,244],{"class":82},[76,663,648],{"class":159},[76,665,666],{"class":82},": post-title; }\n",[76,668,669,672,674,676],{"class":78,"line":100},[76,670,671],{"class":86},".post-detail__title",[76,673,244],{"class":82},[76,675,648],{"class":159},[76,677,666],{"class":82},[12,679,680],{},"两个页面上同名，浏览器就会把它们当成同一个元素，自动做位置和尺寸的插值。",[67,682,684],{"className":145,"code":683,"language":147,"meta":72,"style":72},"::view-transition-old(post-title),\n::view-transition-new(post-title) {\n  animation-duration: 250ms;\n  animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n}\n",[16,685,686,698,707,721,752],{"__ignoreMap":72},[76,687,688,691,695],{"class":78,"line":79},[76,689,690],{"class":82},"::view-transition-old(",[76,692,694],{"class":693},"s9eBZ","post-title",[76,696,697],{"class":82},"),\n",[76,699,700,703,705],{"class":78,"line":100},[76,701,702],{"class":82},"::view-transition-new(",[76,704,694],{"class":693},[76,706,388],{"class":82},[76,708,709,712,714,717,719],{"class":78,"line":107},[76,710,711],{"class":159},"  animation-duration",[76,713,163],{"class":82},[76,715,716],{"class":159},"250",[76,718,169],{"class":93},[76,720,296],{"class":82},[76,722,723,726,728,731,733,736,738,740,742,745,747,750],{"class":78,"line":116},[76,724,725],{"class":159},"  animation-timing-function",[76,727,163],{"class":82},[76,729,730],{"class":159},"cubic-bezier",[76,732,310],{"class":82},[76,734,735],{"class":159},"0.4",[76,737,378],{"class":82},[76,739,252],{"class":159},[76,741,378],{"class":82},[76,743,744],{"class":159},"0.2",[76,746,378],{"class":82},[76,748,749],{"class":159},"1",[76,751,319],{"class":82},[76,753,754],{"class":78,"line":192},[76,755,183],{"class":82},[24,757,758],{"id":758},"必须知道的三个限制",[12,760,761],{},[31,762,763,764,766],{},"一、同一时间只能有一个 ",[16,765,648],{}," 在页面上。",[12,768,769,770,772],{},"如果列表里有 10 张卡片都叫 ",[16,771,694],{},"，浏览器会直接报错、整个过渡失效。所以列表页通常不给每张卡单独命名，只在用户点击时动态设置。",[12,774,775],{},[31,776,777],{},"二、动画期间页面不可交互。",[12,779,780],{},"过渡快照覆盖在真实 DOM 之上。所以别做超过 300ms 的转场。",[12,782,783],{},[31,784,785,786,34],{},"三、必须尊重 ",[16,787,788],{},"prefers-reduced-motion",[67,790,792],{"className":69,"code":791,"language":71,"meta":72,"style":72},"const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches\nconst supported = 'startViewTransition' in document\n\nif (!supported || reduceMotion) {\n  \u002F\u002F 直接切，不做动画\n  applyChange()\n  return\n}\n",[16,793,794,815,833,837,857,862,869,874],{"__ignoreMap":72},[76,795,796,799,802,804,807,809,812],{"class":78,"line":79},[76,797,798],{"class":93},"const",[76,800,801],{"class":159}," reduceMotion",[76,803,399],{"class":93},[76,805,806],{"class":86}," matchMedia",[76,808,310],{"class":82},[76,810,811],{"class":481},"'(prefers-reduced-motion: reduce)'",[76,813,814],{"class":82},").matches\n",[76,816,817,819,822,824,827,830],{"class":78,"line":100},[76,818,798],{"class":93},[76,820,821],{"class":159}," supported",[76,823,399],{"class":93},[76,825,826],{"class":481}," 'startViewTransition'",[76,828,829],{"class":93}," in",[76,831,832],{"class":82}," document\n",[76,834,835],{"class":78,"line":107},[76,836,189],{"emptyLinePlaceholder":188},[76,838,839,842,845,848,851,854],{"class":78,"line":116},[76,840,841],{"class":93},"if",[76,843,844],{"class":82}," (",[76,846,847],{"class":93},"!",[76,849,850],{"class":82},"supported ",[76,852,853],{"class":93},"||",[76,855,856],{"class":82}," reduceMotion) {\n",[76,858,859],{"class":78,"line":192},[76,860,861],{"class":103},"  \u002F\u002F 直接切，不做动画\n",[76,863,864,867],{"class":78,"line":198},[76,865,866],{"class":86},"  applyChange",[76,868,113],{"class":82},[76,870,871],{"class":78,"line":216},[76,872,873],{"class":93},"  return\n",[76,875,876],{"class":78,"line":221},[76,877,183],{"class":82},[12,879,880],{},"这不是可选项。前庭功能障碍的用户会因为这种动画头晕。",[24,882,883],{"id":883},"什么时候不该用",[885,886,887,893,903],"ul",{},[42,888,889,892],{},[31,890,891],{},"表单提交后的状态更新"," —— 不需要转场，用户只想知道成功了没",[42,894,895,898,899,902],{},[31,896,897],{},"列表项的 hover 效果"," —— 那是微交互，用 ",[16,900,901],{},"transition"," 就够了",[42,904,905,908],{},[31,906,907],{},"超过 300ms 的复杂编排"," —— 会让人等",[12,910,911,912,915],{},"View Transitions 的定位是",[31,913,914],{},"页面级的状态切换","，不是\"万能动画方案\"。",[24,917,918],{"id":918},"一句话",[920,921,922],"blockquote",{},[12,923,924],{},"它把\"两套状态之间怎么过渡\"这件麻烦事，从你手里拿走了。",[12,926,927],{},"这可能不是最酷的动画 API，但它是我用过最省心的那个。",[929,930,931],"style",{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#57606A;--shiki-dark:#9AA4B0}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);}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":72,"searchDepth":100,"depth":100,"links":933},[934,935,940,941,942],{"id":26,"depth":100,"text":26},{"id":130,"depth":100,"text":130,"children":936},[937,938,939],{"id":134,"depth":107,"text":135},{"id":336,"depth":107,"text":337},{"id":641,"depth":107,"text":642},{"id":758,"depth":100,"text":758},{"id":883,"depth":100,"text":883},{"id":918,"depth":100,"text":918},"前端","\u002Fimages\u002Fcovers\u002Fview-transition.png","2025-09-18","用它做页面切换、做主题扩散、做列表到详情的共享元素过渡。它真正的价值不是\"让页面动起来\"，而是让你不必手动管理进入和离开两套状态。",false,"md",{},"\u002Fposts\u002F7e4167d8",{"title":5,"description":946},"posts\u002Fview-transitions",[954,955,956],"动画","浏览器","CSS",null,"1sUtwAnQQfrLB9RXq1fOdBX9tXlIxhr_X3vkIqQAj7U",{"prev":960,"next":969},{"path":961,"title":962,"description":963,"date":964,"updated":957,"tags":965,"category":967,"cover":968,"draft":947,"pinned":188,"featured":947},"\u002Fposts\u002F90a855a1","排版比配色更决定气质","把同样一套颜色交给两个人排，出来的东西可以完全不同。这篇讲我怎么用字重、行距和留白，在不换颜色的前提下改变一个页面的脾气。","2025-10-28",[966,967,956],"排版","设计","\u002Fimages\u002Fcovers\u002Ftypography.png",{"path":970,"title":971,"description":972,"date":973,"updated":957,"tags":974,"category":943,"cover":957,"draft":947,"pinned":947,"featured":947},"\u002Fposts\u002Fb240ed92","自托管字体的一年","从 Google Fonts 换成自托管，从自托管换成子集化分片。聊聊为什么一个中文字体能从 25MB 变成实际加载 180KB。","2025-08-08",[975,976,977],"字体","性能","中文",[979,987,989],{"path":980,"title":981,"description":982,"date":983,"updated":957,"tags":984,"category":943,"cover":957,"draft":947,"pinned":947,"featured":947},"\u002Fposts\u002F7d72abe8","我把 CSS 变量当成了 API","设计 token 写进配置文件，构建期编译成 CSS 变量，组件只读变量。这套做法让「改一处，全站生效」从口号变成了构建期保证。","2025-04-20",[956,985,986],"设计系统","构建",{"path":970,"title":971,"description":972,"date":973,"updated":957,"tags":988,"category":943,"cover":957,"draft":947,"pinned":947,"featured":947},[975,976,977],{"path":990,"title":991,"description":992,"date":993,"updated":957,"tags":994,"category":943,"cover":957,"draft":947,"pinned":947,"featured":947},"\u002Fposts\u002F746cfa4c","无加载不是速度，是连续感","一个 300ms 的白屏比一个 800ms 的骨架屏更让人焦虑。这篇讲怎么用 SSG、预取和 view transition 把页面跳转做成「一瞬间的事」。","2025-05-15",[976,995,996],"体验","Nuxt",1794495069899]