{
    "version": "https://jsonfeed.org/version/1",
    "title": "星舞互娱",
    "subtitle": "深夜编码手记",
    "icon": "https://xwhy.online/assets/favicon.ico",
    "description": "在终端与夜色之间，记录构建互联网的每一个细节：前端工程、性能优化与界面设计。",
    "home_page_url": "https://xwhy.online",
    "items": [
        {
            "id": "https://xwhy.online/2026/10/12/rendering-pipeline/",
            "url": "https://xwhy.online/2026/10/12/rendering-pipeline/",
            "title": "把一帧画面拆开看：浏览器渲染流水线完全指南",
            "date_published": "2026-10-12T02:10:00.000Z",
            "content_html": "<p>几乎每一次&quot;页面怎么这么卡&quot;的讨论，最后都会落到同一个问题上：<strong>你到底在第几步花了时间？</strong></p>\n<p>渲染流水线并不是一个抽象的比喻，它是一条真实存在、可以被逐帧观测的链路。理解它，你就不需要靠猜来优化性能。</p>\n<h2 id=\"五个阶段一条流水线\"><a class=\"anchor\" href=\"#五个阶段一条流水线\">#</a> 五个阶段，一条流水线</h2>\n<p>从按下回车到看见像素，浏览器大致会经历：</p>\n<ol>\n<li><strong>Parse</strong> —— 字节流变成 DOM，CSS 变成 CSSOM</li>\n<li><strong>Style</strong> —— 为每个节点计算最终样式（Recalculate Style）</li>\n<li><strong>Layout</strong> —— 计算几何位置，输出盒模型坐标</li>\n<li><strong>Paint</strong> —— 填充像素，生成绘制指令</li>\n<li><strong>Composite</strong> —— 分层合成，交给 GPU 上屏</li>\n</ol>\n<p>关键在于：<strong>后面的阶段依赖前面的阶段</strong>。你改一个属性，触发的阶段越靠后，代价越小。</p>\n<h2 id=\"三种改动三种代价\"><a class=\"anchor\" href=\"#三种改动三种代价\">#</a> 三种改动，三种代价</h2>\n<table>\n<thead>\n<tr>\n<th>你改了什么</th>\n<th>触发的阶段</th>\n<th>代价</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>width</code>、<code>top</code>、字体大小</td>\n<td>Layout → Paint → Composite</td>\n<td>最贵</td>\n</tr>\n<tr>\n<td><code>background-color</code>、<code>box-shadow</code></td>\n<td>Paint → Composite</td>\n<td>中等</td>\n</tr>\n<tr>\n<td><code>transform</code>、<code>opacity</code></td>\n<td>Composite</td>\n<td>最便宜</td>\n</tr>\n</tbody>\n</table>\n<p>所以动画优先用 <code>transform</code> 和 <code>opacity</code>，不是为了&quot;玄学更丝滑&quot;，而是因为它们真的跳过了前两个阶段。</p>\n<pre class=\"shiki shiki-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee\" tabindex=\"0\"><code class=\"language-css\"><span class=\"line\"><span style=\"color:#A0ADA0;--shiki-dark:#758575DD\">/* 会触发 Layout 的写法 */</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">card</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">hover</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#123;</span><span style=\"color:#998418;--shiki-dark:#B8A965\"> left</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\"> 4</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">px</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#125;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#A0ADA0;--shiki-dark:#758575DD\">/* 只走合成层的写法 */</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">card</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#123;</span></span>\n<span class=\"line\"><span style=\"color:#998418;--shiki-dark:#B8A965\">  transition</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> transform </span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\">240</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">ms</span><span style=\"color:#998418;--shiki-dark:#B8A965\"> cubic-bezier</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">(</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\">.22</span><span style=\"color:#999999;--shiki-dark:#666666\">,</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\">.61</span><span style=\"color:#999999;--shiki-dark:#666666\">,</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\">.36</span><span style=\"color:#999999;--shiki-dark:#666666\">,</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\">1</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">)</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"><span style=\"color:#998418;--shiki-dark:#B8A965\">  will-change</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> transform</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#125;</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">card</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">hover</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#123;</span><span style=\"color:#998418;--shiki-dark:#B8A965\"> transform</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#998418;--shiki-dark:#B8A965\"> translate3d</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">(</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\">0</span><span style=\"color:#999999;--shiki-dark:#666666\">,</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\"> -4</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">px</span><span style=\"color:#999999;--shiki-dark:#666666\">,</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\"> 0</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">)</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#125;</span></span></code></pre>\n<blockquote>\n<p>注意：<code>will-change</code> 是一把双刃剑。它会让元素提前被提升为合成层，滥用会吃掉显存，反而拖慢整体帧率。只在真正会动的元素上加，动画结束后考虑移除。</p>\n</blockquote>\n<h2 id=\"强制同步布局最隐蔽的性能杀手\"><a class=\"anchor\" href=\"#强制同步布局最隐蔽的性能杀手\">#</a> 强制同步布局：最隐蔽的性能杀手</h2>\n<p>有一种写法看起来无害，却会让浏览器在同一帧里反复重排：</p>\n<pre class=\"shiki shiki-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee\" tabindex=\"0\"><code class=\"language-js\"><span class=\"line\"><span style=\"color:#A0ADA0;--shiki-dark:#758575DD\">// 每轮循环都在强制 Layout</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">for</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">(</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">const</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> el</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> of</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> list</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">)</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#123;</span></span>\n<span class=\"line\"><span style=\"color:#B07D48;--shiki-dark:#BD976A\">  el</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">style</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">width</span><span style=\"color:#999999;--shiki-dark:#666666\"> =</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> el</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">offsetWidth</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> +</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\"> 10</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> +</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\"> '</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">px</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">'</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#125;</span></span></code></pre>\n<p>读取 <code>offsetWidth</code> 会强制浏览器&quot;立刻&quot;把待处理的样式变更刷新成布局结果。写在循环里，就是 O(n) 次同步布局。</p>\n<p>修法很简单：<strong>先读后写</strong>。</p>\n<pre class=\"shiki shiki-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee\" tabindex=\"0\"><code class=\"language-js\"><span class=\"line\"><span style=\"color:#A0ADA0;--shiki-dark:#758575DD\">// 1. 一次性读完</span></span>\n<span class=\"line\"><span style=\"color:#AB5959;--shiki-dark:#CB7676\">const</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> widths</span><span style=\"color:#999999;--shiki-dark:#666666\"> =</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> list</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#59873A;--shiki-dark:#80A665\">map</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">(</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">(</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">el</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">)</span><span style=\"color:#999999;--shiki-dark:#666666\"> =</span><span style=\"color:#999999;--shiki-dark:#666666\">></span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> el</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">offsetWidth</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">)</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"><span style=\"color:#A0ADA0;--shiki-dark:#758575DD\">// 2. 再统一写</span></span>\n<span class=\"line\"><span style=\"color:#B07D48;--shiki-dark:#BD976A\">list</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#59873A;--shiki-dark:#80A665\">forEach</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">(</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">(</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">el</span><span style=\"color:#999999;--shiki-dark:#666666\">,</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> i</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">)</span><span style=\"color:#999999;--shiki-dark:#666666\"> =</span><span style=\"color:#999999;--shiki-dark:#666666\">></span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">&#123;</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> el</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">style</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">width</span><span style=\"color:#999999;--shiki-dark:#666666\"> =</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> widths</span><span style=\"color:#a65e2b;--shiki-dark:#d4976c\">[</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">i</span><span style=\"color:#a65e2b;--shiki-dark:#d4976c\">]</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> +</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\"> 10</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> +</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\"> '</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">px</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">'</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">&#125;</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">)</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span></code></pre>\n<h2 id=\"怎么用-devtools-抓到真凶\"><a class=\"anchor\" href=\"#怎么用-devtools-抓到真凶\">#</a> 怎么用 DevTools 抓到真凶</h2>\n<p>打开 Performance 面板，录制一段交互，然后看主线程的火焰图：</p>\n<ul>\n<li>黄色块里出现紫色的 <strong>Layout</strong>，说明重排了</li>\n<li>绿色 <strong>Paint</strong> 密集出现，说明绘制压力大</li>\n<li>底部 <strong>Frames</strong> 出现红色三角，那一帧就超时了（&gt;16.7ms）</li>\n</ul>\n<p>把时间花在读数上，而不是花在猜上。</p>\n<h2 id=\"一张检查清单\"><a class=\"anchor\" href=\"#一张检查清单\">#</a> 一张检查清单</h2>\n<ul>\n<li>动画只用 <code>transform</code> / <code>opacity</code></li>\n<li>不在循环里混用读写 DOM</li>\n<li>长列表用虚拟滚动，而不是 <code>display: none</code> 隐藏</li>\n<li>图片给足 <code>width</code> / <code>height</code>，避免布局抖动（CLS）</li>\n<li>离屏内容用 <code>content-visibility: auto</code> 跳过渲染</li>\n</ul>\n<p>渲染没有魔法，只有阶段与代价。把这两件事记住，性能问题就变成了一道可以算的题。</p>\n",
            "tags": [
                "浏览器原理",
                "性能优化",
                "渲染",
                "DevTools"
            ]
        },
        {
            "id": "https://xwhy.online/2026/10/05/css-layers/",
            "url": "https://xwhy.online/2026/10/05/css-layers/",
            "title": "用 CSS Cascade Layers 收拾一个失控的样式表",
            "date_published": "2026-10-05T02:17:00.000Z",
            "content_html": "<p>一个项目跑了两年之后，样式表通常会长成三种东西的混合物：组件样式、工具类，以及一堆用来&quot;压过别人&quot;的补丁。</p>\n<pre class=\"shiki shiki-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee\" tabindex=\"0\"><code class=\"language-css\"><span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">page</span><span style=\"color:#999999;--shiki-dark:#666666\"> .</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">card</span><span style=\"color:#999999;--shiki-dark:#666666\"> .</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">card-body</span><span style=\"color:#999999;--shiki-dark:#666666\"> .</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">btn</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">primary</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#123;</span></span>\n<span class=\"line\"><span style=\"color:#998418;--shiki-dark:#B8A965\">  background</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#999999;--shiki-dark:#666666\"> #</span><span style=\"color:#A65E2B;--shiki-dark:#C99076\">7c5cff</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\"> !important</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#125;</span></span></code></pre>\n<p>写这行代码的人并不想这样，他只是没有别的办法。</p>\n<h2 id=\"layer-把优先级变成声明\"><a class=\"anchor\" href=\"#layer-把优先级变成声明\">#</a> @layer 把优先级变成声明</h2>\n<pre class=\"shiki shiki-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee\" tabindex=\"0\"><code class=\"language-css\"><span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">@</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\">layer</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> reset, base, components, utilities</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span></code></pre>\n<p>这一行的意思是：<strong>后声明的层永远赢，与选择器特异性无关</strong>。</p>\n<p>于是 utilities 里的一个简单类：</p>\n<pre class=\"shiki shiki-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee\" tabindex=\"0\"><code class=\"language-css\"><span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">@</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\">layer</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> utilities </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#123;</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">  .</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">mt-0</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">&#123;</span><span style=\"color:#998418;--shiki-dark:#B8A965\"> margin-top</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\"> 0</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">&#125;</span></span>\n<span class=\"line\"><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#125;</span></span></code></pre>\n<p>可以稳定地压过 components 里那个写了五个类的复杂选择器：</p>\n<pre class=\"shiki shiki-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee\" tabindex=\"0\"><code class=\"language-css\"><span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">@</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\">layer</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> components </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#123;</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">  .</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">page</span><span style=\"color:#999999;--shiki-dark:#666666\"> .</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">card</span><span style=\"color:#999999;--shiki-dark:#666666\"> .</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">card-body</span><span style=\"color:#999999;--shiki-dark:#666666\"> .</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">title</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">&#123;</span><span style=\"color:#998418;--shiki-dark:#B8A965\"> margin-top</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\"> 24</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">px</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">&#125;</span></span>\n<span class=\"line\"><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#125;</span></span></code></pre>\n<p>不需要 <code>!important</code>，也不需要提高特异性。层级关系是显式的，是可 review 的。</p>\n<h2 id=\"推荐的层结构\"><a class=\"anchor\" href=\"#推荐的层结构\">#</a> 推荐的层结构</h2>\n<pre class=\"shiki shiki-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee\" tabindex=\"0\"><code class=\"language-css\"><span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">@</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\">layer</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> reset,</span></span>\n<span class=\"line\"><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">       tokens,</span></span>\n<span class=\"line\"><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">       base,</span></span>\n<span class=\"line\"><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">       layout,</span></span>\n<span class=\"line\"><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">       components,</span></span>\n<span class=\"line\"><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">       utilities,</span></span>\n<span class=\"line\"><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">       overrides;</span></span></code></pre>\n<p>从低到高，语义清晰：</p>\n<ul>\n<li><strong>reset</strong> —— 抹平浏览器差异</li>\n<li><strong>tokens</strong> —— 设计变量，几乎不产生规则</li>\n<li><strong>base</strong> —— 元素默认样式</li>\n<li><strong>layout</strong> —— 栅格、容器</li>\n<li><strong>components</strong> —— 组件自己的一亩三分地</li>\n<li><strong>utilities</strong> —— 单职责工具类，永远能赢</li>\n<li><strong>overrides</strong> —— 临时补丁层，方便日后清理</li>\n</ul>\n<h2 id=\"和第三方库和平共处\"><a class=\"anchor\" href=\"#和第三方库和平共处\">#</a> 和第三方库和平共处</h2>\n<p>这是 @layer 最实用的场景：把 UI 库整个塞进低优先级层。</p>\n<pre class=\"shiki shiki-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee\" tabindex=\"0\"><code class=\"language-css\"><span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">@</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\">import</span><span style=\"color:#998418;--shiki-dark:#B8A965\"> url</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">(</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">'</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">vendor-ui.css</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">'</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">)</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> layer</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">(</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">vendor</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">)</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">@</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\">layer</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> vendor, components, utilities</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span></code></pre>\n<p>库里的 <code>.button &#123; padding: 8px 16px &#125;</code> 依然生效，但你的 <code>.btn-lg</code> 可以轻松覆盖它，不用再写 <code>!important</code> 对抗。</p>\n<blockquote>\n<p>一个容易踩的坑：<code>@import</code> 必须写在所有规则之前。如果打包器把 CSS 拼接顺序打乱，层顺序可能不是你预期的——显式写一行 <code>@layer a, b, c;</code> 声明顺序最保险。</p>\n</blockquote>\n<h2 id=\"渐进增强的写法\"><a class=\"anchor\" href=\"#渐进增强的写法\">#</a> 渐进增强的写法</h2>\n<p>老浏览器不认识 <code>@layer</code>，会直接忽略整条 at-rule 里的内容（而不是报错崩溃）。所以可以这样兜底：</p>\n<pre class=\"shiki shiki-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee\" tabindex=\"0\"><code class=\"language-css\"><span class=\"line\"><span style=\"color:#A0ADA0;--shiki-dark:#758575DD\">/* 老浏览器走这条 */</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">btn</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#123;</span><span style=\"color:#998418;--shiki-dark:#B8A965\"> padding</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\"> 10</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">px</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\"> 20</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">px</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#125;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#A0ADA0;--shiki-dark:#758575DD\">/* 新浏览器用层来管理 */</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">@</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\">layer</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> components </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#123;</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">  .</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">btn</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">&#123;</span><span style=\"color:#998418;--shiki-dark:#B8A965\"> padding</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\"> 10</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">px</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\"> 20</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">px</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">&#125;</span></span>\n<span class=\"line\"><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#125;</span></span></code></pre>\n<p>代价是重复一次声明，换来的是完全不依赖构建工具的兼容性。</p>\n<h2 id=\"迁移顺序建议\"><a class=\"anchor\" href=\"#迁移顺序建议\">#</a> 迁移顺序建议</h2>\n<p>不要一次性重写。按这个顺序动，风险最小：</p>\n<ol>\n<li>先加一行 <code>@layer</code> 声明，什么都不放</li>\n<li>把 reset 和 tokens 搬进去</li>\n<li>把工具类搬进 utilities，删掉对应的 <code>!important</code></li>\n<li>最后处理组件层，一边搬一边删掉为了压优先级而加的选择器</li>\n</ol>\n<p>层叠不是敌人。它只是过去缺少一个可以表达&quot;谁该赢&quot;的语法。现在有了。</p>\n",
            "tags": [
                "CSS",
                "CSS",
                "架构",
                "可维护性"
            ]
        },
        {
            "id": "https://xwhy.online/2026/09/28/view-transitions/",
            "url": "https://xwhy.online/2026/09/28/view-transitions/",
            "title": "View Transitions：给多页应用加一点\"原生感\"",
            "date_published": "2026-09-28T02:24:00.000Z",
            "content_html": "<p>用户判断一个网站&quot;快不快&quot;，很大程度上取决于<strong>切换瞬间发生了什么</strong>。白屏 300ms 会让人觉得慢，哪怕实际加载只用了 200ms。</p>\n<p>View Transitions API 解决的正是这个感知问题。</p>\n<h2 id=\"最小可用示例\"><a class=\"anchor\" href=\"#最小可用示例\">#</a> 最小可用示例</h2>\n<pre class=\"shiki shiki-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee\" tabindex=\"0\"><code class=\"language-js\"><span class=\"line\"><span style=\"color:#AB5959;--shiki-dark:#CB7676\">async</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> function</span><span style=\"color:#59873A;--shiki-dark:#80A665\"> navigate</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">(</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">url</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">)</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#123;</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">  if</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">(</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">!</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">document</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">startViewTransition</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">)</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">&#123;</span></span>\n<span class=\"line\"><span style=\"color:#B07D48;--shiki-dark:#BD976A\">    location</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">href</span><span style=\"color:#999999;--shiki-dark:#666666\"> =</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> url</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">    return</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">  </span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">&#125;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#AB5959;--shiki-dark:#CB7676\">  const</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> res</span><span style=\"color:#999999;--shiki-dark:#666666\"> =</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\"> await</span><span style=\"color:#59873A;--shiki-dark:#80A665\"> fetch</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">(</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">url</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">)</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"><span style=\"color:#AB5959;--shiki-dark:#CB7676\">  const</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> html</span><span style=\"color:#999999;--shiki-dark:#666666\"> =</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\"> await</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> res</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#59873A;--shiki-dark:#80A665\">text</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">(</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">)</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"><span style=\"color:#AB5959;--shiki-dark:#CB7676\">  const</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> doc</span><span style=\"color:#999999;--shiki-dark:#666666\"> =</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> new</span><span style=\"color:#59873A;--shiki-dark:#80A665\"> DOMParser</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">(</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">)</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#59873A;--shiki-dark:#80A665\">parseFromString</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">(</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">html</span><span style=\"color:#999999;--shiki-dark:#666666\">,</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\"> '</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">text/html</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">'</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">)</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#B07D48;--shiki-dark:#BD976A\">  document</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#59873A;--shiki-dark:#80A665\">startViewTransition</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">(</span><span style=\"color:#a65e2b;--shiki-dark:#d4976c\">(</span><span style=\"color:#a65e2b;--shiki-dark:#d4976c\">)</span><span style=\"color:#999999;--shiki-dark:#666666\"> =</span><span style=\"color:#999999;--shiki-dark:#666666\">></span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#a65e2b;--shiki-dark:#d4976c\">&#123;</span></span>\n<span class=\"line\"><span style=\"color:#B07D48;--shiki-dark:#BD976A\">    document</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">body</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#59873A;--shiki-dark:#80A665\">replaceWith</span><span style=\"color:#a13865;--shiki-dark:#d9739f\">(</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">doc</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">body</span><span style=\"color:#a13865;--shiki-dark:#d9739f\">)</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">  </span><span style=\"color:#a65e2b;--shiki-dark:#d4976c\">&#125;</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">)</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#125;</span></span></code></pre>\n<p>浏览器会自动做三件事：截取旧状态快照、执行你的 DOM 变更、截取新状态快照，然后在两张快照之间做交叉淡入。</p>\n<h2 id=\"给元素命名才有共享元素动画\"><a class=\"anchor\" href=\"#给元素命名才有共享元素动画\">#</a> 给元素命名，才有共享元素动画</h2>\n<p>默认是整页淡入淡出。想让标题&quot;飞&quot;过去，需要 <code>view-transition-name</code>：</p>\n<pre class=\"shiki shiki-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee\" tabindex=\"0\"><code class=\"language-css\"><span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">post-title</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#123;</span><span style=\"color:#998418;--shiki-dark:#B8A965\"> view-transition-name</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> post-title</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#125;</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">post-cover</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#123;</span><span style=\"color:#998418;--shiki-dark:#B8A965\"> view-transition-name</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> post-cover</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#125;</span></span></code></pre>\n<p>两个页面上同名，浏览器就会把它们当成同一个元素做位移动画。<strong>同一时刻页面上不能有两个同名元素</strong>，否则转场直接失效——这是最常见的翻车点。</p>\n<h2 id=\"三个容易翻车的细节\"><a class=\"anchor\" href=\"#三个容易翻车的细节\">#</a> 三个容易翻车的细节</h2>\n<h3 id=\"1-滚动位置\"><a class=\"anchor\" href=\"#1-滚动位置\">#</a> 1. 滚动位置</h3>\n<p>转场期间页面被截图，滚动会被重置。记得恢复：</p>\n<pre class=\"shiki shiki-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee\" tabindex=\"0\"><code class=\"language-js\"><span class=\"line\"><span style=\"color:#B07D48;--shiki-dark:#BD976A\">document</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#59873A;--shiki-dark:#80A665\">startViewTransition</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">(</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">(</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">)</span><span style=\"color:#999999;--shiki-dark:#666666\"> =</span><span style=\"color:#999999;--shiki-dark:#666666\">></span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">&#123;</span></span>\n<span class=\"line\"><span style=\"color:#B07D48;--shiki-dark:#BD976A\">  document</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">body</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#59873A;--shiki-dark:#80A665\">replaceWith</span><span style=\"color:#a65e2b;--shiki-dark:#d4976c\">(</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">doc</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">body</span><span style=\"color:#a65e2b;--shiki-dark:#d4976c\">)</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"><span style=\"color:#B07D48;--shiki-dark:#BD976A\">  window</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#59873A;--shiki-dark:#80A665\">scrollTo</span><span style=\"color:#a65e2b;--shiki-dark:#d4976c\">(</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\">0</span><span style=\"color:#999999;--shiki-dark:#666666\">,</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\"> 0</span><span style=\"color:#a65e2b;--shiki-dark:#d4976c\">)</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"><span style=\"color:#1e754f;--shiki-dark:#4d9375\">&#125;</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">)</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span></code></pre>\n<h3 id=\"2-动画时长与可达性\"><a class=\"anchor\" href=\"#2-动画时长与可达性\">#</a> 2. 动画时长与可达性</h3>\n<pre class=\"shiki shiki-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee\" tabindex=\"0\"><code class=\"language-css\"><span class=\"line\"><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">::view-transition-old</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">(</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">root</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">)</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">,</span></span>\n<span class=\"line\"><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">::view-transition-new</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">(</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">root</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">)</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#123;</span></span>\n<span class=\"line\"><span style=\"color:#998418;--shiki-dark:#B8A965\">  animation-duration</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\"> 320</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">ms</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#125;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">@</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\">media</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">(</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">prefers-reduced-motion</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> reduce</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">)</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#123;</span></span>\n<span class=\"line\"><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">  ::view-transition-group</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">(</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\">*</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">)</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">,</span></span>\n<span class=\"line\"><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">  ::view-transition-old</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">(</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\">*</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">)</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">,</span></span>\n<span class=\"line\"><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">  ::view-transition-new</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">(</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\">*</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">)</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\"> </span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">&#123;</span></span>\n<span class=\"line\"><span style=\"color:#998418;--shiki-dark:#B8A965\">    animation</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#A65E2B;--shiki-dark:#C99076\"> none</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\"> !important</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">  </span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">&#125;</span></span>\n<span class=\"line\"><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#125;</span></span></code></pre>\n<p>前庭功能敏感的用户会因位移动画不适，这段媒体查询不是可选项。</p>\n<h3 id=\"3-别让转场挡住交互\"><a class=\"anchor\" href=\"#3-别让转场挡住交互\">#</a> 3. 别让转场挡住交互</h3>\n<p>转场期间页面是&quot;活的快照&quot;，用户点击无效。如果 fetch 很慢，先请求再启动转场，不要先启动再等待：</p>\n<pre class=\"shiki shiki-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee\" tabindex=\"0\"><code class=\"language-js\"><span class=\"line\"><span style=\"color:#A0ADA0;--shiki-dark:#758575DD\">// ✅ 数据就绪后再开始，转场只覆盖 DOM 替换那一瞬</span></span>\n<span class=\"line\"><span style=\"color:#AB5959;--shiki-dark:#CB7676\">const</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> html</span><span style=\"color:#999999;--shiki-dark:#666666\"> =</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\"> await</span><span style=\"color:#59873A;--shiki-dark:#80A665\"> fetchHtml</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">(</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">url</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">)</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"><span style=\"color:#B07D48;--shiki-dark:#BD976A\">document</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#59873A;--shiki-dark:#80A665\">startViewTransition</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">(</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">(</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">)</span><span style=\"color:#999999;--shiki-dark:#666666\"> =</span><span style=\"color:#999999;--shiki-dark:#666666\">></span><span style=\"color:#59873A;--shiki-dark:#80A665\"> swap</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">(</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">html</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">)</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">)</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span></code></pre>\n<h2 id=\"mpa-版本一行-css\"><a class=\"anchor\" href=\"#mpa-版本一行-css\">#</a> MPA 版本：一行 CSS</h2>\n<p>如果站点是多页的，Chromium 已经支持跨文档转场：</p>\n<pre class=\"shiki shiki-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee\" tabindex=\"0\"><code class=\"language-css\"><span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">@</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\">view-transition</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#123;</span></span>\n<span class=\"line\"><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">  navigation: auto;</span></span>\n<span class=\"line\"><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#125;</span></span></code></pre>\n<p>加到两个页面的 CSS 里，站内跳转自动获得淡入效果。不支持的环境什么都不会发生，天然渐进增强。</p>\n<p>体验优化最划算的地方，往往不是让事情变快，而是让等待变得可以被理解。</p>\n",
            "tags": [
                "交互体验",
                "动画",
                "API",
                "渐进增强"
            ]
        },
        {
            "id": "https://xwhy.online/2026/09/19/typescript-narrowing/",
            "url": "https://xwhy.online/2026/09/19/typescript-narrowing/",
            "title": "TypeScript 类型收窄：让编译器替你证明",
            "date_published": "2026-09-19T02:31:00.000Z",
            "content_html": "<p>写 TypeScript 三年之后，你会发现大部分 <code>as</code> 都是同一个原因：<strong>你比编译器更早知道了类型，但没有告诉它</strong>。</p>\n<p>收窄（narrowing）就是告诉它的正确方式。</p>\n<h2 id=\"从最朴素的开始\"><a class=\"anchor\" href=\"#从最朴素的开始\">#</a> 从最朴素的开始</h2>\n<pre class=\"shiki shiki-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee\" tabindex=\"0\"><code class=\"language-ts\"><span class=\"line\"><span style=\"color:#AB5959;--shiki-dark:#CB7676\">type</span><span style=\"color:#2E8F82;--shiki-dark:#5DA994\"> Result</span><span style=\"color:#999999;--shiki-dark:#666666\"> =</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">  |</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#123;</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> ok</span><span style=\"color:#999999;--shiki-dark:#666666\">: </span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">true</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> data</span><span style=\"color:#999999;--shiki-dark:#666666\">: </span><span style=\"color:#2E8F82;--shiki-dark:#5DA994\">string</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#125;</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">  |</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#123;</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> ok</span><span style=\"color:#999999;--shiki-dark:#666666\">: </span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">false</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> error</span><span style=\"color:#999999;--shiki-dark:#666666\">: </span><span style=\"color:#2E8F82;--shiki-dark:#5DA994\">Error</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#125;</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#AB5959;--shiki-dark:#CB7676\">function</span><span style=\"color:#59873A;--shiki-dark:#80A665\"> unwrap</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">(</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">r</span><span style=\"color:#999999;--shiki-dark:#666666\">: </span><span style=\"color:#2E8F82;--shiki-dark:#5DA994\">Result</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">)</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#123;</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">  if</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">(</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">r</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">ok</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">)</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">&#123;</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">    return</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> r</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">data</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span><span style=\"color:#A0ADA0;--shiki-dark:#758575DD\">   // 这里 r 被收窄为 &#123; ok: true; data: string &#125;</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">  </span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">&#125;</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">  throw</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> r</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">error</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span><span style=\"color:#A0ADA0;--shiki-dark:#758575DD\">     // 这里只剩 &#123; ok: false; error: Error &#125;</span></span>\n<span class=\"line\"><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#125;</span></span></code></pre>\n<p>判别字段（discriminant）用字面量类型，是最高效的收窄方式。设计联合类型时，第一件事就是给它一个判别字段。</p>\n<h2 id=\"六种收窄手段\"><a class=\"anchor\" href=\"#六种收窄手段\">#</a> 六种收窄手段</h2>\n<ol>\n<li><strong>typeof</strong> —— 基础类型判断</li>\n<li><strong>instanceof</strong> —— 类实例</li>\n<li><strong>in</strong> —— 属性存在性</li>\n<li><strong>判别字段</strong> —— 字面量联合</li>\n<li><strong>自定义类型守卫</strong>（<code>x is T</code>）</li>\n<li><strong>断言函数</strong>（<code>asserts x is T</code>）</li>\n</ol>\n<p>自定义守卫值得多说一句：</p>\n<pre class=\"shiki shiki-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee\" tabindex=\"0\"><code class=\"language-ts\"><span class=\"line\"><span style=\"color:#AB5959;--shiki-dark:#CB7676\">function</span><span style=\"color:#59873A;--shiki-dark:#80A665\"> isUser</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">(</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">v</span><span style=\"color:#999999;--shiki-dark:#666666\">: </span><span style=\"color:#2E8F82;--shiki-dark:#5DA994\">unknown</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">)</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> v</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> is</span><span style=\"color:#2E8F82;--shiki-dark:#5DA994\"> User</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#123;</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">  return</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">(</span></span>\n<span class=\"line\"><span style=\"color:#AB5959;--shiki-dark:#CB7676\">    typeof</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> v</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> ===</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\"> '</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">object</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">'</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> &#x26;&#x26;</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> v</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> !==</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> null</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> &#x26;&#x26;</span></span>\n<span class=\"line\"><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">    '</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">id</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">'</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> in</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> v</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> &#x26;&#x26;</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> typeof</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#a65e2b;--shiki-dark:#d4976c\">(</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">v</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\"> as</span><span style=\"color:#2E8F82;--shiki-dark:#5DA994\"> User</span><span style=\"color:#a65e2b;--shiki-dark:#d4976c\">)</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">id</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> ===</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\"> '</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">string</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">'</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">  </span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">)</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#125;</span></span></code></pre>\n<p>注意 <strong>返回值类型是 <code>v is User</code> 而不是 <code>boolean</code></strong>。写成 <code>boolean</code>，调用处拿不到任何收窄，等于白写。</p>\n<h2 id=\"never让遗漏变成编译错误\"><a class=\"anchor\" href=\"#never让遗漏变成编译错误\">#</a> never：让遗漏变成编译错误</h2>\n<pre class=\"shiki shiki-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee\" tabindex=\"0\"><code class=\"language-ts\"><span class=\"line\"><span style=\"color:#AB5959;--shiki-dark:#CB7676\">type</span><span style=\"color:#2E8F82;--shiki-dark:#5DA994\"> Shape</span><span style=\"color:#999999;--shiki-dark:#666666\"> =</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">  |</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#123;</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> kind</span><span style=\"color:#999999;--shiki-dark:#666666\">: </span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">'</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">circle</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">'</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> r</span><span style=\"color:#999999;--shiki-dark:#666666\">: </span><span style=\"color:#2E8F82;--shiki-dark:#5DA994\">number</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#125;</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">  |</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#123;</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> kind</span><span style=\"color:#999999;--shiki-dark:#666666\">: </span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">'</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">square</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">'</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> size</span><span style=\"color:#999999;--shiki-dark:#666666\">: </span><span style=\"color:#2E8F82;--shiki-dark:#5DA994\">number</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#125;</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">  |</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#123;</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> kind</span><span style=\"color:#999999;--shiki-dark:#666666\">: </span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">'</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">rect</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">'</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> w</span><span style=\"color:#999999;--shiki-dark:#666666\">: </span><span style=\"color:#2E8F82;--shiki-dark:#5DA994\">number</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> h</span><span style=\"color:#999999;--shiki-dark:#666666\">: </span><span style=\"color:#2E8F82;--shiki-dark:#5DA994\">number</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#125;</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#AB5959;--shiki-dark:#CB7676\">function</span><span style=\"color:#59873A;--shiki-dark:#80A665\"> area</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">(</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">s</span><span style=\"color:#999999;--shiki-dark:#666666\">: </span><span style=\"color:#2E8F82;--shiki-dark:#5DA994\">Shape</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">)</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#2E8F82;--shiki-dark:#5DA994\"> number</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#123;</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">  switch</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">(</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">s</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">kind</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">)</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">&#123;</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">    case</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\"> '</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">circle</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">'</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\"> return</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> Math</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">PI</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> *</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> s</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">r</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> **</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\"> 2</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">    case</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\"> '</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">square</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">'</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\"> return</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> s</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">size</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> **</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\"> 2</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">    case</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\"> '</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">rect</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">'</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\">   return</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> s</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">w</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> *</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> s</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">h</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">    default</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span></span>\n<span class=\"line\"><span style=\"color:#A0ADA0;--shiki-dark:#758575DD\">      // 如果将来有人加了 'triangle' 却没改这里，</span></span>\n<span class=\"line\"><span style=\"color:#A0ADA0;--shiki-dark:#758575DD\">      // 下面这行会立刻报错，而不是静默返回 undefined</span></span>\n<span class=\"line\"><span style=\"color:#AB5959;--shiki-dark:#CB7676\">      const </span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">_exhaustive</span><span style=\"color:#999999;--shiki-dark:#666666\">: </span><span style=\"color:#2E8F82;--shiki-dark:#5DA994\">never</span><span style=\"color:#999999;--shiki-dark:#666666\"> =</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> s</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">      throw</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> new</span><span style=\"color:#59873A;--shiki-dark:#80A665\"> Error</span><span style=\"color:#a65e2b;--shiki-dark:#d4976c\">(</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">'</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">Unhandled shape: </span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">'</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> +</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> JSON</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#59873A;--shiki-dark:#80A665\">stringify</span><span style=\"color:#a13865;--shiki-dark:#d9739f\">(</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">_exhaustive</span><span style=\"color:#a13865;--shiki-dark:#d9739f\">)</span><span style=\"color:#a65e2b;--shiki-dark:#d4976c\">)</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">  </span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">&#125;</span></span>\n<span class=\"line\"><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#125;</span></span></code></pre>\n<p>这是类型系统里最便宜的一道回归测试。</p>\n<h2 id=\"一个真实的重构案例\"><a class=\"anchor\" href=\"#一个真实的重构案例\">#</a> 一个真实的重构案例</h2>\n<p>改造前：</p>\n<pre class=\"shiki shiki-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee\" tabindex=\"0\"><code class=\"language-ts\"><span class=\"line\"><span style=\"color:#AB5959;--shiki-dark:#CB7676\">function</span><span style=\"color:#59873A;--shiki-dark:#80A665\"> render</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">(</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">data</span><span style=\"color:#999999;--shiki-dark:#666666\">: </span><span style=\"color:#2E8F82;--shiki-dark:#5DA994\">any</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">)</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#123;</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">  if</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">(</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">data</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">type</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> ===</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\"> '</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">text</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">'</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">)</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\"> return</span><span style=\"color:#59873A;--shiki-dark:#80A665\"> renderText</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">(</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">data</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">value</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">)</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">  if</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">(</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">data</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">type</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> ===</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\"> '</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">image</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">'</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">)</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\"> return</span><span style=\"color:#59873A;--shiki-dark:#80A665\"> renderImage</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">(</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">data</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">url</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">)</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">  return</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\"> null</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#125;</span></span></code></pre>\n<p>改造后：</p>\n<pre class=\"shiki shiki-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee\" tabindex=\"0\"><code class=\"language-ts\"><span class=\"line\"><span style=\"color:#AB5959;--shiki-dark:#CB7676\">type</span><span style=\"color:#2E8F82;--shiki-dark:#5DA994\"> Block</span><span style=\"color:#999999;--shiki-dark:#666666\"> =</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">  |</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#123;</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> type</span><span style=\"color:#999999;--shiki-dark:#666666\">: </span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">'</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">text</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">'</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> value</span><span style=\"color:#999999;--shiki-dark:#666666\">: </span><span style=\"color:#2E8F82;--shiki-dark:#5DA994\">string</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#125;</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">  |</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#123;</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> type</span><span style=\"color:#999999;--shiki-dark:#666666\">: </span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">'</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">image</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">'</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> url</span><span style=\"color:#999999;--shiki-dark:#666666\">: </span><span style=\"color:#2E8F82;--shiki-dark:#5DA994\">string</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> alt</span><span style=\"color:#999999;--shiki-dark:#666666\">: </span><span style=\"color:#2E8F82;--shiki-dark:#5DA994\">string</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#125;</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#AB5959;--shiki-dark:#CB7676\">function</span><span style=\"color:#59873A;--shiki-dark:#80A665\"> render</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">(</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">block</span><span style=\"color:#999999;--shiki-dark:#666666\">: </span><span style=\"color:#2E8F82;--shiki-dark:#5DA994\">Block</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">)</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#2E8F82;--shiki-dark:#5DA994\"> string</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#123;</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">  switch</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">(</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">block</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">type</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">)</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">&#123;</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">    case</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\"> '</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">text</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">'</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\">  return</span><span style=\"color:#59873A;--shiki-dark:#80A665\"> renderText</span><span style=\"color:#a65e2b;--shiki-dark:#d4976c\">(</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">block</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">value</span><span style=\"color:#a65e2b;--shiki-dark:#d4976c\">)</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">    case</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\"> '</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">image</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">'</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\"> return</span><span style=\"color:#59873A;--shiki-dark:#80A665\"> renderImage</span><span style=\"color:#a65e2b;--shiki-dark:#d4976c\">(</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">block</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">url</span><span style=\"color:#999999;--shiki-dark:#666666\">,</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> block</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">alt</span><span style=\"color:#a65e2b;--shiki-dark:#d4976c\">)</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">  </span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">&#125;</span></span>\n<span class=\"line\"><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#125;</span></span></code></pre>\n<p>调用处传错字段，编译期就会红。运行时 <code>null</code> 分支消失，<code>?.和 as</code> 也一起消失。</p>\n<h2 id=\"收窄的边界\"><a class=\"anchor\" href=\"#收窄的边界\">#</a> 收窄的边界</h2>\n<p>收窄只在<strong>同一作用域、且值不可变</strong>时可靠。回调函数里的收窄会被编译器丢弃：</p>\n<pre class=\"shiki shiki-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee\" tabindex=\"0\"><code class=\"language-ts\"><span class=\"line\"><span style=\"color:#AB5959;--shiki-dark:#CB7676\">function</span><span style=\"color:#59873A;--shiki-dark:#80A665\"> handle</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">(</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">x</span><span style=\"color:#999999;--shiki-dark:#666666\">: </span><span style=\"color:#2E8F82;--shiki-dark:#5DA994\">string</span><span style=\"color:#999999;--shiki-dark:#666666\"> | </span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">null</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">)</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#123;</span></span>\n<span class=\"line\"><span style=\"color:#1E754F;--shiki-dark:#4D9375\">  if</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">(</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">!</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">x</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">)</span><span style=\"color:#1E754F;--shiki-dark:#4D9375\"> return</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#A0ADA0;--shiki-dark:#758575DD\">  // ❌ 报错：回调可能在 x 被修改后执行</span></span>\n<span class=\"line\"><span style=\"color:#59873A;--shiki-dark:#80A665\">  setTimeout</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">(</span><span style=\"color:#a65e2b;--shiki-dark:#d4976c\">(</span><span style=\"color:#a65e2b;--shiki-dark:#d4976c\">)</span><span style=\"color:#999999;--shiki-dark:#666666\"> =</span><span style=\"color:#999999;--shiki-dark:#666666\">></span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> console</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#59873A;--shiki-dark:#80A665\">log</span><span style=\"color:#a65e2b;--shiki-dark:#d4976c\">(</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">x</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#59873A;--shiki-dark:#80A665\">trim</span><span style=\"color:#a13865;--shiki-dark:#d9739f\">(</span><span style=\"color:#a13865;--shiki-dark:#d9739f\">)</span><span style=\"color:#a65e2b;--shiki-dark:#d4976c\">)</span><span style=\"color:#999999;--shiki-dark:#666666\">,</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\"> 0</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">)</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#A0ADA0;--shiki-dark:#758575DD\">  // ✅ 先存到常量</span></span>\n<span class=\"line\"><span style=\"color:#AB5959;--shiki-dark:#CB7676\">  const </span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">value</span><span style=\"color:#999999;--shiki-dark:#666666\"> =</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> x</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"><span style=\"color:#59873A;--shiki-dark:#80A665\">  setTimeout</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">(</span><span style=\"color:#a65e2b;--shiki-dark:#d4976c\">(</span><span style=\"color:#a65e2b;--shiki-dark:#d4976c\">)</span><span style=\"color:#999999;--shiki-dark:#666666\"> =</span><span style=\"color:#999999;--shiki-dark:#666666\">></span><span style=\"color:#B07D48;--shiki-dark:#BD976A\"> console</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#59873A;--shiki-dark:#80A665\">log</span><span style=\"color:#a65e2b;--shiki-dark:#d4976c\">(</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">value</span><span style=\"color:#999999;--shiki-dark:#666666\">.</span><span style=\"color:#59873A;--shiki-dark:#80A665\">trim</span><span style=\"color:#a13865;--shiki-dark:#d9739f\">(</span><span style=\"color:#a13865;--shiki-dark:#d9739f\">)</span><span style=\"color:#a65e2b;--shiki-dark:#d4976c\">)</span><span style=\"color:#999999;--shiki-dark:#666666\">,</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\"> 0</span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">)</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#125;</span></span></code></pre>\n<p>理解这一点，你就不会再写无意义的 <code>!</code>。</p>\n<p>类型不是给编译器看的装饰，它是你写下的、可被机器验证的假设。</p>\n",
            "tags": [
                "TypeScript",
                "TypeScript",
                "类型系统",
                "工程化"
            ]
        },
        {
            "id": "https://xwhy.online/2026/09/08/design-tokens/",
            "url": "https://xwhy.online/2026/09/08/design-tokens/",
            "title": "设计变量不是 CSS 变量换个名字",
            "date_published": "2026-09-08T02:38:00.000Z",
            "content_html": "<p><code>--color-primary</code> 是最容易被滥用的变量名。它的问题不是不好用，而是<strong>它把决策和取值绑死了</strong>。</p>\n<h2 id=\"三层结构\"><a class=\"anchor\" href=\"#三层结构\">#</a> 三层结构</h2>\n<p>一个能长期维护的 token 体系，通常有三层：</p>\n<h3 id=\"1-原始值primitive\"><a class=\"anchor\" href=\"#1-原始值primitive\">#</a> 1. 原始值（primitive）</h3>\n<p>只描述&quot;这是什么颜色&quot;，不带语义。</p>\n<pre class=\"shiki shiki-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee\" tabindex=\"0\"><code class=\"language-css\"><span class=\"line\"><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">--color-violet-500: </span><span style=\"color:#999999;--shiki-light-font-style:italic;--shiki-dark:#666666;--shiki-dark-font-style:italic\">#</span><span style=\"color:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic\">7c5cff</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">;</span></span>\n<span class=\"line\"><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">--color-cyan-400: </span><span style=\"color:#999999;--shiki-light-font-style:italic;--shiki-dark:#666666;--shiki-dark-font-style:italic\">#</span><span style=\"color:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic\">22d3ee</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">;</span></span>\n<span class=\"line\"><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">--color-slate-950: </span><span style=\"color:#999999;--shiki-light-font-style:italic;--shiki-dark:#666666;--shiki-dark-font-style:italic\">#</span><span style=\"color:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic\">0a0b10</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">;</span></span></code></pre>\n<p>这一层不直接被组件使用。</p>\n<h3 id=\"2-语义层semantic\"><a class=\"anchor\" href=\"#2-语义层semantic\">#</a> 2. 语义层（semantic）</h3>\n<p>描述&quot;用在哪里&quot;。</p>\n<pre class=\"shiki shiki-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee\" tabindex=\"0\"><code class=\"language-css\"><span class=\"line\"><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">--surface-base: var</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">(</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">--color-slate-950</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">)</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">;</span></span>\n<span class=\"line\"><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">--surface-raised: </span><span style=\"color:#999999;--shiki-light-font-style:italic;--shiki-dark:#666666;--shiki-dark-font-style:italic\">#</span><span style=\"color:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic\">12141c</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">;</span></span>\n<span class=\"line\"><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">--text-primary: </span><span style=\"color:#999999;--shiki-light-font-style:italic;--shiki-dark:#666666;--shiki-dark-font-style:italic\">#</span><span style=\"color:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic\">eef1f8;</span></span>\n<span class=\"line\"><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">--text-muted: </span><span style=\"color:#999999;--shiki-light-font-style:italic;--shiki-dark:#666666;--shiki-dark-font-style:italic\">#</span><span style=\"color:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic\">98a2b8</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">;</span></span>\n<span class=\"line\"><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">--accent: var</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">(</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">--color-violet-500</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">)</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">;</span></span>\n<span class=\"line\"><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">--border-subtle: rgba</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">(</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">255</span><span style=\"color:#999999;--shiki-dark:#666666\">,</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">255</span><span style=\"color:#999999;--shiki-dark:#666666\">,</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">255</span><span style=\"color:#999999;--shiki-dark:#666666\">,</span><span style=\"color:#999999;--shiki-light-font-style:italic;--shiki-dark:#666666;--shiki-dark-font-style:italic\">.</span><span style=\"color:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic\">08</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">)</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">;</span></span></code></pre>\n<h3 id=\"3-组件层component\"><a class=\"anchor\" href=\"#3-组件层component\">#</a> 3. 组件层（component）</h3>\n<p>只在必要时存在，用来说明&quot;这个组件怎么用语义 token&quot;。</p>\n<pre class=\"shiki shiki-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee\" tabindex=\"0\"><code class=\"language-css\"><span class=\"line\"><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">--card-bg: var</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">(</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">--surface-raised</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">)</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">;</span></span>\n<span class=\"line\"><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">--card-border: var</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">(</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">--border-subtle</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">)</span><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">;</span></span>\n<span class=\"line\"><span style=\"color:#393A34;--shiki-dark:#DBD7CAEE\">--card-radius: 16px;</span></span></code></pre>\n<h2 id=\"为什么不能只有一层\"><a class=\"anchor\" href=\"#为什么不能只有一层\">#</a> 为什么不能只有一层</h2>\n<p>假设你只有 <code>--color-primary</code>。有三处需要&quot;稍微淡一点的边框&quot;，你怎么命名？</p>\n<ul>\n<li><code>--color-primary-10</code>？</li>\n<li><code>--border-light</code>？</li>\n<li><code>--color-gray-800</code>？</li>\n</ul>\n<p>没有语义层，命名就会开始描述<strong>程度</strong>而不是<strong>用途</strong>，很快就没人知道该用哪个了。</p>\n<p>而有了 <code>--border-subtle</code>，深浅调整只发生在一个地方。</p>\n<h2 id=\"暗色主题不是反色\"><a class=\"anchor\" href=\"#暗色主题不是反色\">#</a> 暗色主题不是反色</h2>\n<p>换肤最常见的错误做法，是把所有颜色取反。结果通常是：纯黑背景 + 纯白文字，对比度过高，长时间阅读眼睛疼。</p>\n<p>正确的做法是在语义层做映射：</p>\n<pre class=\"shiki shiki-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee\" tabindex=\"0\"><code class=\"language-css\"><span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">root</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#123;</span></span>\n<span class=\"line\"><span style=\"color:#B07D48;--shiki-dark:#BD976A\">  --surface-base</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#999999;--shiki-dark:#666666\"> #</span><span style=\"color:#A65E2B;--shiki-dark:#C99076\">f7f7f9</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"><span style=\"color:#B07D48;--shiki-dark:#BD976A\">  --text-primary</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#999999;--shiki-dark:#666666\"> #</span><span style=\"color:#A65E2B;--shiki-dark:#C99076\">14161c</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"><span style=\"color:#B07D48;--shiki-dark:#BD976A\">  --text-muted</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#999999;--shiki-dark:#666666\"> #</span><span style=\"color:#A65E2B;--shiki-dark:#C99076\">5b6472</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"><span style=\"color:#B07D48;--shiki-dark:#BD976A\">  --accent</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#999999;--shiki-dark:#666666\"> #</span><span style=\"color:#A65E2B;--shiki-dark:#C99076\">6d4aff</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#125;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">[</span><span style=\"color:#B07D48;--shiki-dark:#BD976A\">data-theme</span><span style=\"color:#AB5959;--shiki-dark:#CB7676\">=</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">'</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">dark</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">'</span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">]</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#123;</span></span>\n<span class=\"line\"><span style=\"color:#B07D48;--shiki-dark:#BD976A\">  --surface-base</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#999999;--shiki-dark:#666666\"> #</span><span style=\"color:#A65E2B;--shiki-dark:#C99076\">0a0b10</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span><span style=\"color:#A0ADA0;--shiki-dark:#758575DD\">   /* 不是 #000 */</span></span>\n<span class=\"line\"><span style=\"color:#B07D48;--shiki-dark:#BD976A\">  --text-primary</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#999999;--shiki-dark:#666666\"> #</span><span style=\"color:#A65E2B;--shiki-dark:#C99076\">eef1f8</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span><span style=\"color:#A0ADA0;--shiki-dark:#758575DD\">   /* 不是 #fff */</span></span>\n<span class=\"line\"><span style=\"color:#B07D48;--shiki-dark:#BD976A\">  --text-muted</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#999999;--shiki-dark:#666666\"> #</span><span style=\"color:#A65E2B;--shiki-dark:#C99076\">98a2b8</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span></span>\n<span class=\"line\"><span style=\"color:#B07D48;--shiki-dark:#BD976A\">  --accent</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#999999;--shiki-dark:#666666\"> #</span><span style=\"color:#A65E2B;--shiki-dark:#C99076\">8b74ff</span><span style=\"color:#999999;--shiki-dark:#666666\">;</span><span style=\"color:#A0ADA0;--shiki-dark:#758575DD\">         /* 暗底上提亮，保证对比度 */</span></span>\n<span class=\"line\"><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#125;</span></span></code></pre>\n<p>组件完全不感知主题，只引用语义 token。</p>\n<h2 id=\"增量落地\"><a class=\"anchor\" href=\"#增量落地\">#</a> 增量落地</h2>\n<p>不要为了 token 化停下来两周做重构。可以这样切：</p>\n<ol>\n<li>把现有硬编码颜色按出现频次排序，取前 20 个</li>\n<li>为它们建立语义名，先在旧代码里引入变量但保持取值不变</li>\n<li>新代码强制走 token，旧代码在改动时顺手迁移</li>\n<li>加一条 lint 规则：组件 CSS 里出现十六进制颜色就警告</li>\n</ol>\n<p>半年之后，你会发现换主题只需要改二十行。</p>\n",
            "tags": [
                "设计工程",
                "CSS",
                "架构",
                "设计系统"
            ]
        },
        {
            "id": "https://xwhy.online/2026/08/30/web-performance-budget/",
            "url": "https://xwhy.online/2026/08/30/web-performance-budget/",
            "title": "性能预算：把\"感觉慢\"变成一条可以报警的红线",
            "date_published": "2026-08-30T02:45:00.000Z",
            "content_html": "<p>&quot;这个页面有点慢&quot; —— 这句话在评审会上没有杀伤力，因为它无法被反驳，也无法被验证。</p>\n<p>性能预算的作用，是把它变成&quot;LCP 超过 2.5s，构建失败&quot;。</p>\n<h2 id=\"先定指标再定阈值\"><a class=\"anchor\" href=\"#先定指标再定阈值\">#</a> 先定指标，再定阈值</h2>\n<p>三个核心指标，建议按这个优先级强制：</p>\n<table>\n<thead>\n<tr>\n<th>指标</th>\n<th>含义</th>\n<th>目标</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>LCP</td>\n<td>最大内容绘制</td>\n<td>&lt; 2.5s</td>\n</tr>\n<tr>\n<td>CLS</td>\n<td>累积布局偏移</td>\n<td>&lt; 0.1</td>\n</tr>\n<tr>\n<td>INP</td>\n<td>交互到下次绘制</td>\n<td>&lt; 200ms</td>\n</tr>\n</tbody>\n</table>\n<p>再加两个资源预算，兜住&quot;悄悄变胖&quot;：</p>\n<ul>\n<li>首屏 JS gzip 后 &lt; 180KB</li>\n<li>首屏图片总量 &lt; 500KB</li>\n</ul>\n<h2 id=\"用-lighthouse-ci-落地\"><a class=\"anchor\" href=\"#用-lighthouse-ci-落地\">#</a> 用 Lighthouse CI 落地</h2>\n<pre class=\"shiki shiki-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee\" tabindex=\"0\"><code class=\"language-bash\"><span class=\"line\"><span style=\"color:#59873A;--shiki-dark:#80A665\">npm</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\"> i</span><span style=\"color:#A65E2B;--shiki-dark:#C99076\"> -D</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\"> @lhci/cli</span></span></code></pre>\n<p><code>lighthouserc.json</code>：</p>\n<pre class=\"shiki shiki-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee\" tabindex=\"0\"><code class=\"language-json\"><span class=\"line\"><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#123;</span></span>\n<span class=\"line\"><span style=\"color:#99841877;--shiki-dark:#B8A96577\">  \"</span><span style=\"color:#998418;--shiki-dark:#B8A965\">ci</span><span style=\"color:#99841877;--shiki-dark:#B8A96577\">\"</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">&#123;</span></span>\n<span class=\"line\"><span style=\"color:#99841877;--shiki-dark:#B8A96577\">    \"</span><span style=\"color:#998418;--shiki-dark:#B8A965\">collect</span><span style=\"color:#99841877;--shiki-dark:#B8A96577\">\"</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#a65e2b;--shiki-dark:#d4976c\">&#123;</span></span>\n<span class=\"line\"><span style=\"color:#99841877;--shiki-dark:#B8A96577\">      \"</span><span style=\"color:#998418;--shiki-dark:#B8A965\">url</span><span style=\"color:#99841877;--shiki-dark:#B8A96577\">\"</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#a13865;--shiki-dark:#d9739f\">[</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">\"</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">http://localhost:4173/</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">\"</span><span style=\"color:#999999;--shiki-dark:#666666\">,</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\"> \"</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">http://localhost:4173/post.html?slug=css-layers</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">\"</span><span style=\"color:#a13865;--shiki-dark:#d9739f\">]</span><span style=\"color:#999999;--shiki-dark:#666666\">,</span></span>\n<span class=\"line\"><span style=\"color:#99841877;--shiki-dark:#B8A96577\">      \"</span><span style=\"color:#998418;--shiki-dark:#B8A965\">numberOfRuns</span><span style=\"color:#99841877;--shiki-dark:#B8A96577\">\"</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\"> 3</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">    </span><span style=\"color:#a65e2b;--shiki-dark:#d4976c\">&#125;</span><span style=\"color:#999999;--shiki-dark:#666666\">,</span></span>\n<span class=\"line\"><span style=\"color:#99841877;--shiki-dark:#B8A96577\">    \"</span><span style=\"color:#998418;--shiki-dark:#B8A965\">assert</span><span style=\"color:#99841877;--shiki-dark:#B8A96577\">\"</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#a65e2b;--shiki-dark:#d4976c\">&#123;</span></span>\n<span class=\"line\"><span style=\"color:#99841877;--shiki-dark:#B8A96577\">      \"</span><span style=\"color:#998418;--shiki-dark:#B8A965\">assertions</span><span style=\"color:#99841877;--shiki-dark:#B8A96577\">\"</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#a13865;--shiki-dark:#d9739f\">&#123;</span></span>\n<span class=\"line\"><span style=\"color:#99841877;--shiki-dark:#B8A96577\">        \"</span><span style=\"color:#998418;--shiki-dark:#B8A965\">categories:performance</span><span style=\"color:#99841877;--shiki-dark:#B8A96577\">\"</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#bda437;--shiki-dark:#e6cc77\">[</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">\"</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">error</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">\"</span><span style=\"color:#999999;--shiki-dark:#666666\">,</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#296aa3;--shiki-dark:#6394bf\">&#123;</span><span style=\"color:#99841877;--shiki-dark:#B8A96577\"> \"</span><span style=\"color:#998418;--shiki-dark:#B8A965\">minScore</span><span style=\"color:#99841877;--shiki-dark:#B8A96577\">\"</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\"> 0.9</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#296aa3;--shiki-dark:#6394bf\">&#125;</span><span style=\"color:#bda437;--shiki-dark:#e6cc77\">]</span><span style=\"color:#999999;--shiki-dark:#666666\">,</span></span>\n<span class=\"line\"><span style=\"color:#99841877;--shiki-dark:#B8A96577\">        \"</span><span style=\"color:#998418;--shiki-dark:#B8A965\">largest-contentful-paint</span><span style=\"color:#99841877;--shiki-dark:#B8A96577\">\"</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#bda437;--shiki-dark:#e6cc77\">[</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">\"</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">error</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">\"</span><span style=\"color:#999999;--shiki-dark:#666666\">,</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#296aa3;--shiki-dark:#6394bf\">&#123;</span><span style=\"color:#99841877;--shiki-dark:#B8A96577\"> \"</span><span style=\"color:#998418;--shiki-dark:#B8A965\">maxNumericValue</span><span style=\"color:#99841877;--shiki-dark:#B8A96577\">\"</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\"> 2500</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#296aa3;--shiki-dark:#6394bf\">&#125;</span><span style=\"color:#bda437;--shiki-dark:#e6cc77\">]</span><span style=\"color:#999999;--shiki-dark:#666666\">,</span></span>\n<span class=\"line\"><span style=\"color:#99841877;--shiki-dark:#B8A96577\">        \"</span><span style=\"color:#998418;--shiki-dark:#B8A965\">cumulative-layout-shift</span><span style=\"color:#99841877;--shiki-dark:#B8A96577\">\"</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#bda437;--shiki-dark:#e6cc77\">[</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">\"</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">error</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">\"</span><span style=\"color:#999999;--shiki-dark:#666666\">,</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#296aa3;--shiki-dark:#6394bf\">&#123;</span><span style=\"color:#99841877;--shiki-dark:#B8A96577\"> \"</span><span style=\"color:#998418;--shiki-dark:#B8A965\">maxNumericValue</span><span style=\"color:#99841877;--shiki-dark:#B8A96577\">\"</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\"> 0.1</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#296aa3;--shiki-dark:#6394bf\">&#125;</span><span style=\"color:#bda437;--shiki-dark:#e6cc77\">]</span><span style=\"color:#999999;--shiki-dark:#666666\">,</span></span>\n<span class=\"line\"><span style=\"color:#99841877;--shiki-dark:#B8A96577\">        \"</span><span style=\"color:#998418;--shiki-dark:#B8A965\">total-blocking-time</span><span style=\"color:#99841877;--shiki-dark:#B8A96577\">\"</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#bda437;--shiki-dark:#e6cc77\">[</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">\"</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">warn</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">\"</span><span style=\"color:#999999;--shiki-dark:#666666\">,</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#296aa3;--shiki-dark:#6394bf\">&#123;</span><span style=\"color:#99841877;--shiki-dark:#B8A96577\"> \"</span><span style=\"color:#998418;--shiki-dark:#B8A965\">maxNumericValue</span><span style=\"color:#99841877;--shiki-dark:#B8A96577\">\"</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#2F798A;--shiki-dark:#4C9A91\"> 300</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#296aa3;--shiki-dark:#6394bf\">&#125;</span><span style=\"color:#bda437;--shiki-dark:#e6cc77\">]</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">      </span><span style=\"color:#a13865;--shiki-dark:#d9739f\">&#125;</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">    </span><span style=\"color:#a65e2b;--shiki-dark:#d4976c\">&#125;</span><span style=\"color:#999999;--shiki-dark:#666666\">,</span></span>\n<span class=\"line\"><span style=\"color:#99841877;--shiki-dark:#B8A96577\">    \"</span><span style=\"color:#998418;--shiki-dark:#B8A965\">upload</span><span style=\"color:#99841877;--shiki-dark:#B8A96577\">\"</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#a65e2b;--shiki-dark:#d4976c\">&#123;</span><span style=\"color:#99841877;--shiki-dark:#B8A96577\"> \"</span><span style=\"color:#998418;--shiki-dark:#B8A965\">target</span><span style=\"color:#99841877;--shiki-dark:#B8A96577\">\"</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\"> \"</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\">temporary-public-storage</span><span style=\"color:#B5695977;--shiki-dark:#C98A7D77\">\"</span><span style=\"color:#999999;--shiki-dark:#666666\"> </span><span style=\"color:#a65e2b;--shiki-dark:#d4976c\">&#125;</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">  </span><span style=\"color:#1e754f;--shiki-dark:#4d9375\">&#125;</span></span>\n<span class=\"line\"><span style=\"color:#2993a3;--shiki-dark:#5eaab5\">&#125;</span></span></code></pre>\n<p>跑三次取中位数，避免单次网络抖动造成误报。</p>\n<h2 id=\"在-ci-里卡住回归\"><a class=\"anchor\" href=\"#在-ci-里卡住回归\">#</a> 在 CI 里卡住回归</h2>\n<pre class=\"shiki shiki-themes vitesse-light vitesse-dark\" style=\"background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee\" tabindex=\"0\"><code class=\"language-yaml\"><span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">-</span><span style=\"color:#998418;--shiki-dark:#B8A965\"> name</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\"> Build</span></span>\n<span class=\"line\"><span style=\"color:#998418;--shiki-dark:#B8A965\">  run</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\"> npm run build</span></span>\n<span class=\"line\"><span style=\"color:#999999;--shiki-dark:#666666\">-</span><span style=\"color:#998418;--shiki-dark:#B8A965\"> name</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\"> Performance budget</span></span>\n<span class=\"line\"><span style=\"color:#998418;--shiki-dark:#B8A965\">  run</span><span style=\"color:#999999;--shiki-dark:#666666\">:</span><span style=\"color:#B56959;--shiki-dark:#C98A7D\"> npx lhci autorun</span></span></code></pre>\n<p>第一次接入建议先设成 <code>warn</code>，观察两周真实分布，再把稳定项升级成 <code>error</code>。一上来就 red，团队会习惯性跳过它。</p>\n<h2 id=\"三个立刻见效的动作\"><a class=\"anchor\" href=\"#三个立刻见效的动作\">#</a> 三个立刻见效的动作</h2>\n<ol>\n<li><strong>给图片加尺寸和懒加载</strong>——直接改善 CLS 与 LCP</li>\n<li><strong>首屏字体用 <code>font-display: swap</code></strong>——避免文字闪烁</li>\n<li><strong>把非关键脚本改成 <code>defer</code></strong>——减少主线程阻塞</li>\n</ol>\n<blockquote>\n<p>性能预算真正的作用不是让分数变高，而是让&quot;这次改动值不值得&quot;变成一个有数字可对的讨论。</p>\n</blockquote>\n<p>预算写进 CI 的那天，性能才从&quot;有人在意&quot;变成&quot;项目属性&quot;。</p>\n",
            "tags": [
                "性能优化",
                "性能优化",
                "工程化",
                "CI/CD"
            ]
        }
    ]
}