<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>性能优化 on Zampo Blog</title><link>https://blog.cpdd.fyi/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/</link><description>Recent content in 性能优化 on Zampo Blog</description><generator>Hugo</generator><language>zh-cn</language><lastBuildDate>Thu, 28 May 2026 09:40:00 +0800</lastBuildDate><atom:link href="https://blog.cpdd.fyi/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/index.xml" rel="self" type="application/rss+xml"/><item><title>GC 调优到底先看什么：Go 为什么要从 live heap 开始，而不是先改 GOGC</title><link>https://blog.cpdd.fyi/posts/go-gc-tuning-workflow/</link><pubDate>Thu, 28 May 2026 09:40:00 +0800</pubDate><guid>https://blog.cpdd.fyi/posts/go-gc-tuning-workflow/</guid><description>&lt;p&gt;上篇讲完 &lt;code&gt;GOGC&lt;/code&gt;、&lt;code&gt;GOMEMLIMIT&lt;/code&gt; 和 STW，很多人真正卡住的地方才刚开始。&lt;/p&gt;
&lt;p&gt;不是“不知道这些参数是什么”，而是线上真的出问题时，不知道下一步该干什么。&lt;/p&gt;
&lt;p&gt;服务 p99 每隔几十秒抖一下，GC CPU 看起来有 5%。有人说把 &lt;code&gt;GOGC&lt;/code&gt; 从 100 调到 200，有人说先上 &lt;code&gt;GOMEMLIMIT&lt;/code&gt;，有人说 Go GC 已经很强，肯定不是它的问题。最后开会半小时，结论往往是：先改一个数试试。&lt;/p&gt;
&lt;p&gt;这才是 GC 调优最危险的地方。&lt;/p&gt;
&lt;p&gt;不是参数难，而是决策路径太松。你不知道自己是在省 CPU，还是在借内存；不知道问题是活对象太多，还是短命对象太多；也不知道什么时候应该停手。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;没有路径的调优，就是拿线上流量做抽签。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这篇不再展开概念。我们只解决一个问题：当你已经知道 &lt;code&gt;GOGC&lt;/code&gt; 和 &lt;code&gt;GOMEMLIMIT&lt;/code&gt; 是什么，线上到底怎么用。&lt;/p&gt;
&lt;h2 id="第一步先证明-gc-真的在场"&gt;第一步：先证明 GC 真的在场&lt;/h2&gt;
&lt;p&gt;很多排查一开始就走偏，是因为团队先认定“这就是 GC 问题”。&lt;/p&gt;
&lt;p&gt;GC CPU 有 5%，不等于它一定是罪魁祸首；GC CPU 只有 1%，也不等于它一定无关。线上要看的不是某个数字高不高，而是它和业务曲线有没有关系。&lt;/p&gt;
&lt;p&gt;先把三条线叠起来看：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;p99 / p999 抖动时间点
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;GC 周期和标记阶段时间点
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;CPU、RSS、heap live 的同步变化
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;临时排查可以先开 &lt;code&gt;gctrace&lt;/code&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;GODEBUG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;gctrace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; ./your-service
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;你可能会看到类似这样的行：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;gc 12 @8.7s 2%: 0.021+4.3+0.065 ms clock, 64-&amp;gt;72-&amp;gt;38 MB, 76 MB goal
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这行不用每个字段都背下来。先抓三件事：&lt;/p&gt;</description></item><item><title>Go 为什么调高 GOGC 会换来 OOM：GOMEMLIMIT 不是替你付账的人</title><link>https://blog.cpdd.fyi/posts/go-gc-gogc-gomemlimit/</link><pubDate>Thu, 28 May 2026 09:20:00 +0800</pubDate><guid>https://blog.cpdd.fyi/posts/go-gc-gogc-gomemlimit/</guid><description>&lt;p&gt;服务 p99 每隔几十秒抖一下。&lt;/p&gt;
&lt;p&gt;监控里 GC CPU 大概 5%，不算离谱，但曲线很准：GC 一来，延迟就往上冒。群里很快有人提议，把 &lt;code&gt;GOGC&lt;/code&gt; 从 100 调到 200，先让 GC 少跑几次。&lt;/p&gt;
&lt;p&gt;这个动作看起来很合理。&lt;/p&gt;
&lt;p&gt;CPU 下来了，GC 日志没那么密了，p99 也安静了几天。然后另一个问题来了：容器内存开始贴近 limit，某个高峰期直接 OOMKilled。大家又把 &lt;code&gt;GOGC&lt;/code&gt; 调回去，延迟抖动也跟着回来。&lt;/p&gt;
&lt;p&gt;这就是很多 Go GC 调优的真实状态：不是不会调参数，而是没想清楚自己到底在买什么。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GOGC 调的是账期，不是魔法。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;你把它调高，本质上是允许堆多长一段时间，用内存换 CPU；你把它调低，本质上是让 GC 更勤快，用 CPU 换内存。至于 &lt;code&gt;GOMEMLIMIT&lt;/code&gt;，它也不是另一个版本的 &lt;code&gt;GOGC&lt;/code&gt;，而是在告诉 runtime：Go 运行时能看见的那部分内存，最好别越过这条线。&lt;/p&gt;
&lt;p&gt;所以真正的问题不是“GOGC 到底设多少”。&lt;/p&gt;
&lt;p&gt;真正的问题是：这笔账，现在谁付得起？&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.cpdd.fyi/images/go-gc-gogc-gomemlimit/inline-01.png" alt="GOGC、GOMEMLIMIT 与容器 limit 的关系"&gt;&lt;/p&gt;
&lt;h2 id="gogc-调的是账期不是回收能力"&gt;GOGC 调的是账期，不是回收能力&lt;/h2&gt;
&lt;p&gt;Go 官方 GC Guide 里有一个很关键的目标堆公式，可以先这样理解：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Target heap memory = Live heap + (Live heap + GC roots) × GOGC / 100
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;不用背公式，抓住账本就行。&lt;/p&gt;</description></item><item><title>Go GC 为什么不能只看 STW：三色标记和写屏障才是你该先算的账</title><link>https://blog.cpdd.fyi/posts/go-gc-three-color-concurrent/</link><pubDate>Wed, 27 May 2026 11:15:00 +0800</pubDate><guid>https://blog.cpdd.fyi/posts/go-gc-three-color-concurrent/</guid><description>&lt;p&gt;服务内存从 800MB 慢慢涨到 2.6GB。&lt;/p&gt;
&lt;p&gt;曲线不陡，不像雪崩。它只是每天往上爬一点，重启以后掉下来，过几天又回到老地方。告警一响，群里很快会出现两个动作：先怀疑泄漏，再问 &lt;code&gt;GOGC&lt;/code&gt; 要不要调。&lt;/p&gt;
&lt;p&gt;这两个反应都不离谱，但都太早。&lt;/p&gt;
&lt;p&gt;在 Go 服务里，内存涨可能是真泄漏，也可能只是 live heap 变大了；可能是分配速率太高，也可能是 GC 目标允许堆多长了一段；还有一种常见误会：堆已经回收了，但 RSS 没有按你期待的节奏马上降下去。&lt;/p&gt;
&lt;p&gt;真正要命的不是内存涨，而是你把 GC 当黑箱。&lt;/p&gt;
&lt;p&gt;一旦把它当黑箱，排查就会变成猜参数：&lt;code&gt;GOGC&lt;/code&gt; 调大一点？调小一点？要不要加 &lt;code&gt;GOMEMLIMIT&lt;/code&gt;？要不要手动 &lt;code&gt;runtime.GC()&lt;/code&gt;？&lt;/p&gt;
&lt;p&gt;这些动作不是不能做。问题是，在你没看懂 GC 正在算什么账之前，调参很容易变成给线上服务摇骰子。&lt;/p&gt;
&lt;h2 id="gc-不是保洁员是成本调度器"&gt;GC 不是保洁员，是成本调度器&lt;/h2&gt;
&lt;p&gt;很多人对 GC 的第一印象是“内存不用了，运行时帮我扫掉”。这个说法只说中了一半。&lt;/p&gt;
&lt;p&gt;另一半更重要：GC 每工作一次，都要付成本。&lt;/p&gt;
&lt;p&gt;扫得太勤，CPU 被回收器吃掉，吞吐会掉，延迟也可能抖。扫得太晚，内存峰值会上去，容器可能还没等你优雅回收，就先把进程杀掉。&lt;/p&gt;
&lt;p&gt;所以 Go GC 不像保洁员，看到垃圾就立刻冲上去扫。它更像一个拿着预算表的人：上一轮还有多少对象活着？这轮又分配了多少？再拖一会儿，内存扛不扛得住？现在开始扫，CPU 又吃不吃得消？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GC 调的不是洁癖，是成本。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;GOGC&lt;/code&gt; 就是这笔账里最常见的旋钮。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;GOGC=100&lt;/code&gt; 不是“内存到 100MB 就触发 GC”。它更接近一个增长比例：在上一轮存活堆的基础上，允许新分配的堆内存再长一段，再启动下一轮 GC。Go 官方 GC Guide 给出的目标堆公式可以简化理解为：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;目标堆 ≈ live heap + (live heap + GC roots) × GOGC / 100
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这里有个很容易误会的地方：这个目标不是你容器的内存上限，也不是进程 RSS 上限。它只是运行时决定“什么时候该开始下一轮 GC”的一个目标。&lt;/p&gt;</description></item><item><title>Go 1.25 的 map 已经换了一套发动机：Swiss Table 到底改了什么</title><link>https://blog.cpdd.fyi/posts/go-swiss-map-design/</link><pubDate>Sat, 23 May 2026 14:45:00 +0800</pubDate><guid>https://blog.cpdd.fyi/posts/go-swiss-map-design/</guid><description>&lt;p&gt;你读过的很多 Go map 文章，可能已经过时了。&lt;/p&gt;
&lt;p&gt;它们还在讲 &lt;code&gt;hmap&lt;/code&gt;、&lt;code&gt;bmap&lt;/code&gt;、overflow bucket、load factor 6.5，还会画一张 bucket 后面挂着 overflow bucket 的图。那些内容不是没有价值。问题在于，从 Go 1.24 开始，默认 map 实现已经换成 Swiss Table；到了 Go 1.25，再把旧模型当成主线讲，就会把读者带偏。&lt;/p&gt;
&lt;p&gt;map 的 API 没变。&lt;code&gt;make(map[K]V)&lt;/code&gt; 还是那个写法，&lt;code&gt;m[k]&lt;/code&gt; 还是查找，&lt;code&gt;delete(m, k)&lt;/code&gt; 还是删除，&lt;code&gt;range m&lt;/code&gt; 还是遍历。&lt;/p&gt;
&lt;p&gt;但发动机已经换了。&lt;/p&gt;
&lt;p&gt;旧模型里，map 更像一排 bucket，每个 bucket 装几个 key/value，冲突多了就挂 overflow bucket。新模型里，Go map 的核心变成了 Swiss Table：开放寻址、group、control word、H1/H2、多 table、extendible hashing。&lt;/p&gt;
&lt;p&gt;这不是“源码爱好者才需要知道”的细节。你排查性能、解释内存占用、写技术分享，甚至只是判断某篇文章能不能信，都得先问一句：它讲的是 Go 1.24 以前的默认实现，还是现在的 Swiss map？&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.cpdd.fyi/images/go-swiss-map-design/cover.png" alt="Go map 发动机更换"&gt;&lt;/p&gt;
&lt;h2 id="旧-hmap-没消失但它已经不是默认主线"&gt;旧 hmap 没消失，但它已经不是默认主线&lt;/h2&gt;
&lt;p&gt;先把边界说清楚：旧的 &lt;code&gt;hmap + bmap + overflow bucket&lt;/code&gt; 不是错。&lt;/p&gt;
&lt;p&gt;它曾经是 Go map 的默认实现。今天在 Go 1.25.4 的源码里，旧实现也还在，文件名很直接：&lt;code&gt;runtime/map_noswiss.go&lt;/code&gt;。问题是，它不再是默认路径。Go 1.24 的发布说明已经把 built-in map 的新 Swiss Table 实现作为默认行为；本机 Go 1.25.4 源码里，新的入口是 &lt;code&gt;runtime/map_swiss.go&lt;/code&gt;，真正的核心实现则在 &lt;code&gt;internal/runtime/maps&lt;/code&gt; 下面。&lt;/p&gt;</description></item><item><title>Go 服务被 OOMKilled，别先怪 GC</title><link>https://blog.cpdd.fyi/posts/go-gc-debugging/</link><pubDate>Fri, 22 May 2026 00:01:00 +0800</pubDate><guid>https://blog.cpdd.fyi/posts/go-gc-debugging/</guid><description>&lt;p&gt;服务又被 OOMKilled 了。&lt;/p&gt;
&lt;p&gt;Pod 刚重启，群里已经有人开始翻 GC 日志。有人说 &lt;code&gt;GC CPU&lt;/code&gt; 到了 20%，有人说把 &lt;code&gt;GOGC&lt;/code&gt; 调低一点，还有人建议直接上 &lt;code&gt;GOMEMLIMIT&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这些动作不一定错，但顺序错了。&lt;/p&gt;
&lt;p&gt;OOMKilled 是结果，不是根因。它只说明容器被内核杀掉了，不说明是谁把内存吃掉了。可能是 Go heap 真的在涨，可能是 goroutine 泄漏把栈和引用一起拖大，可能是 &lt;code&gt;alloc_space&lt;/code&gt; 很高导致 GC 忙，可能是 RSS 里有 cgo、mmap、tmpfs，也可能只是容器 limit 给得太紧。&lt;/p&gt;
&lt;p&gt;Go 服务的内存问题，最怕一句话定性。&lt;/p&gt;
&lt;p&gt;一旦你把所有问题都叫“GC 问题”，排查就会变成调参赌博：今天调 &lt;code&gt;GOGC&lt;/code&gt;，明天加 limit，后天又开始怀疑 pprof 没抓准。真正稳定的做法，是先把几条线拆开：Go 堆、Go runtime 管理的内存、RSS、容器 limit，以及业务生命周期。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.cpdd.fyi/images/go-gc-debugging/debug-flow.png" alt="Go GC 排查流程"&gt;&lt;/p&gt;
&lt;h2 id="第一步不是调参数是确认谁杀了你"&gt;第一步不是调参数，是确认谁杀了你&lt;/h2&gt;
&lt;p&gt;线上看到内存上涨，第一件事不是打开 pprof，也不是改 &lt;code&gt;GOGC&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;先确认事故事实。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl describe pod &amp;lt;pod&amp;gt; -n &amp;lt;ns&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl logs &amp;lt;pod&amp;gt; -n &amp;lt;ns&amp;gt; --previous
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl top pod &amp;lt;pod&amp;gt; -n &amp;lt;ns&amp;gt; --containers
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl get pod &amp;lt;pod&amp;gt; -n &amp;lt;ns&amp;gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -o &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;{.status.containerStatuses[*].lastState.terminated.reason}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;你要先回答几个问题：&lt;/p&gt;</description></item><item><title>Go GC 占 CPU 5%，到底要不要调 GOGC？</title><link>https://blog.cpdd.fyi/posts/go-gc-tuning/</link><pubDate>Thu, 21 May 2026 23:30:00 +0800</pubDate><guid>https://blog.cpdd.fyi/posts/go-gc-tuning/</guid><description>&lt;p&gt;服务 p99 每隔几十秒抖一下。&lt;/p&gt;
&lt;p&gt;监控里 GC CPU 大概 5%，不算夸张，但曲线很准：GC 一来，延迟就往上冒。群里很快有人提议：把 &lt;code&gt;GOGC&lt;/code&gt; 从 100 调到 200，先让 GC 少跑几次。&lt;/p&gt;
&lt;p&gt;这个动作看起来很合理。&lt;/p&gt;
&lt;p&gt;CPU 下来了，GC 日志没那么密了，p99 也安静了几天。然后另一个问题来了：容器内存开始逼近 limit，某个高峰期被 OOMKilled。大家又把 &lt;code&gt;GOGC&lt;/code&gt; 调回去，延迟抖动也跟着回来。&lt;/p&gt;
&lt;p&gt;这就是很多 Go GC 调优的真实状态：不是不会调参数，而是没想清楚自己到底在买什么。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;GOGC&lt;/code&gt; 调的是账期，不是魔法。&lt;/p&gt;
&lt;p&gt;调高它，相当于允许堆多长一段时间，用内存换 CPU；调低它，相当于让 GC 更勤快，用 CPU 换内存。至于 &lt;code&gt;GOMEMLIMIT&lt;/code&gt;，它不是另一个版本的 &lt;code&gt;GOGC&lt;/code&gt;，而是在告诉 runtime：这个进程由 Go 管的内存，最好别越过这条线。&lt;/p&gt;
&lt;p&gt;所以这篇不打算把 GC 参数逐个解释一遍。线上真正需要的是一条决策路径：要不要调，调什么，调多少，什么时候停手。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.cpdd.fyi/images/go-gc-tuning/gogc-heap-curve.png" alt="GOGC 与堆目标关系"&gt;&lt;/p&gt;
&lt;h2 id="先别问-gogc-设多少先问谁在付账"&gt;先别问 GOGC 设多少，先问谁在付账&lt;/h2&gt;
&lt;p&gt;Go 官方 GC Guide 给过一个很关键的公式：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Target heap memory = Live heap + (Live heap + GC roots) × GOGC / 100
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这句话不用背，抓住意思就行。&lt;/p&gt;</description></item><item><title>内存一直涨，不一定是泄漏：Go GC 真正在做什么</title><link>https://blog.cpdd.fyi/posts/go-gc-three-color-marking/</link><pubDate>Thu, 21 May 2026 20:25:00 +0800</pubDate><guid>https://blog.cpdd.fyi/posts/go-gc-three-color-marking/</guid><description>&lt;p&gt;服务内存从 800MB 慢慢涨到 2.6GB。&lt;/p&gt;
&lt;p&gt;曲线不陡，不像雪崩。它只是每天往上爬一点，重启以后掉下来，过几天又回到老地方。告警响了，群里第一句话通常是：是不是内存泄漏？&lt;/p&gt;
&lt;p&gt;这个判断不算错，但太急。&lt;/p&gt;
&lt;p&gt;在 Go 服务里，内存持续上涨可能是真泄漏，也可能是 live heap 变大了，可能是 &lt;code&gt;GOGC&lt;/code&gt; 目标让下一轮回收来得更晚，也可能是短命对象太多，把 GC 拖进了频繁工作。还有一种更容易误判：heap 已经回收了，但 RSS 没有马上按你想象的方式降下来。&lt;/p&gt;
&lt;p&gt;内存涨，不等于内存泄漏。&lt;/p&gt;
&lt;p&gt;要把这件事查清楚，不能只盯 RSS。你得知道 Go GC 什么时候启动、怎么判断对象还活着、为什么需要写屏障、哪里会停顿，以及哪些参数是真的能调。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.cpdd.fyi/images/go-gc-three-color-marking/gc-flow.svg" alt="Go GC 流程图"&gt;&lt;/p&gt;
&lt;h2 id="gc-不是保洁员是成本调度器"&gt;GC 不是保洁员，是成本调度器&lt;/h2&gt;
&lt;p&gt;很多人对 GC 的直觉是“内存不用了，运行时帮我扫掉”。这个说法只对了一半。&lt;/p&gt;
&lt;p&gt;真正重要的是另一半：GC 每扫一次，都要花 CPU；扫得太勤，CPU 被回收器吃掉，服务吞吐和延迟会受影响。扫得太晚，内存峰值又会涨上去，容器可能先把你杀掉。&lt;/p&gt;
&lt;p&gt;所以 Go GC 不是等地上脏了才来的保洁员。它更像一个拿着预算表的人：上一轮还有多少对象活着？再往后拖一点，内存能不能接受？现在开始扫，CPU 能不能接受？&lt;/p&gt;
&lt;p&gt;Go 官方 GC Guide 讲得很清楚：GC 主要消耗 CPU 和物理内存。想省 CPU，就得允许堆多长一会儿；想省内存，就得让 GC 更频繁地工作。&lt;/p&gt;
&lt;p&gt;这就是 &lt;code&gt;GOGC&lt;/code&gt; 的本质。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;GOGC=100&lt;/code&gt; 不是“内存达到 100MB 触发 GC”。它的意思更接近：在上一轮存活堆的基础上，允许新分配对象按约 100% 的比例增长，再触发下一轮 GC。Go 1.19 之后，目标堆计算还会把 GC roots 的成本纳入进来，可以简化理解为：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;下一轮目标 ≈ live heap + (live heap + GC roots) × GOGC / 100
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;GOGC&lt;/code&gt; 调的是成本，不是情绪。&lt;/p&gt;</description></item><item><title>你的服务不是 CPU 不够快，是缺页中断在报警</title><link>https://blog.cpdd.fyi/posts/virtual-memory-debugging/</link><pubDate>Thu, 23 Apr 2026 18:00:00 +0800</pubDate><guid>https://blog.cpdd.fyi/posts/virtual-memory-debugging/</guid><description>&lt;p&gt;凌晨两点，线上服务突然变慢。&lt;/p&gt;
&lt;p&gt;你登上服务器，top 一看：CPU 占用 30%，内存也没满，磁盘 IO 正常。但请求就是卡，P99 从 50ms 跳到 800ms。&lt;/p&gt;
&lt;p&gt;第一反应：加机器？&lt;/p&gt;
&lt;p&gt;先别急。系统给你的信号不是&amp;quot;资源不够&amp;quot;，是&amp;quot;内存用错了&amp;quot;——&lt;strong&gt;缺页中断在报警，你听不懂而已。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;虚拟内存不是操作系统课程里的理论。它是每个写代码的人都应该能用来诊断问题的工具。&lt;/p&gt;
&lt;p&gt;但 99% 的开发者都不理解虚拟内存。&lt;/p&gt;
&lt;p&gt;你是那 1% 吗？&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="一虚拟内存到底是什么"&gt;一、虚拟内存到底是什么&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;虚拟内存，就是让程序以为自己有很多内存，实际上用的是磁盘空间来凑。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;你的机器只有 8GB RAM，但可以运行需要 12GB 内存的程序。多出来的 4GB 从哪来？从硬盘的交换空间（swap）借。&lt;/p&gt;
&lt;p&gt;这不是魔法，是交换。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 看看你的 swap 用了多少&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;free -h
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;输出：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; total used free shared buff/cache available
Mem: 15Gi 8.2Gi 2.1Gi 1.2Gi 5.7Gi 6.8Gi
Swap: 8.0Gi 1.5Gi 6.5Gi
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;看到 &lt;code&gt;Swap used 1.5Gi&lt;/code&gt; 了吗？这意味着有 1.5GB 的数据被从 RAM 换出到磁盘了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;经验值：&lt;/strong&gt; swap 使用率超过 30%，服务延迟大概率会抖。不是绝对，但值得警惕。&lt;/p&gt;</description></item><item><title>99% 的开发者不懂堆内存，你真的懂吗？</title><link>https://blog.cpdd.fyi/posts/heap-memory-deep-dive/</link><pubDate>Fri, 17 Apr 2026 18:00:00 +0800</pubDate><guid>https://blog.cpdd.fyi/posts/heap-memory-deep-dive/</guid><description>&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;example&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;问你一个问题：这个 &lt;code&gt;x&lt;/code&gt; 是在堆上还是在栈上？&lt;/p&gt;
&lt;p&gt;如果你答&amp;quot;栈上&amp;quot;，你错了——至少在这段代码里，Go 编译器会把 &lt;code&gt;x&lt;/code&gt; 分配到堆上。&lt;/p&gt;
&lt;p&gt;为什么？因为它的地址逃逸了函数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;99% 的开发者不懂堆内存&lt;/strong&gt;——不是不懂 &lt;code&gt;malloc&lt;/code&gt; 和 &lt;code&gt;free&lt;/code&gt;，是不懂分配器、碎片化、逃逸分析、GC 代价这些让堆变得迷人的底层细节。&lt;/p&gt;
&lt;p&gt;你可能觉得内存管理就是&amp;quot;分配&amp;quot;和&amp;quot;释放&amp;quot;这么简单，也可能不在意这些底层细节。但如果你不懂我刚才说的那些术语，或者想深入了解堆内存，这篇就是为你写的。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="一堆-vs-栈核心差异在哪"&gt;一、堆 vs 栈：核心差异在哪&lt;/h2&gt;
&lt;p&gt;先说结论。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;栈内存&lt;/strong&gt;是程序内存中用于存储局部变量的区域，&lt;strong&gt;堆内存&lt;/strong&gt;是用于动态分配的区域。&lt;/p&gt;
&lt;p&gt;听起来简单，但 99% 的开发者忽略了两者的本质差异。&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;差异点&lt;/th&gt;
 &lt;th&gt;栈（Stack）&lt;/th&gt;
 &lt;th&gt;堆（Heap）&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;管理方式&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;后进先出（LIFO），编译器自动管理&lt;/td&gt;
 &lt;td&gt;运行时管理，分配器或 GC 管理&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;分配速度&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;极快，编译器确切知道值存储在哪里、存活多久&lt;/td&gt;
 &lt;td&gt;慢，涉及元数据、可能的线程同步、GC&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;生命周期&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;函数结束时自动释放&lt;/td&gt;
 &lt;td&gt;可以比创建它的函数活得更久&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;大小限制&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;相对较小，通常几 MB&lt;/td&gt;
 &lt;td&gt;更大、更灵活，可向 OS 请求更多&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;碎片化&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;无&lt;/td&gt;
 &lt;td&gt;有，频繁分配释放会产生碎片&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;栈的工作方式：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;每次函数调用都会压入一个帧（frame），包含局部变量，返回时弹出。这个过程是严格的后继先出，所以极快。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;堆的工作方式：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;堆是一个更灵活但代价更高的空间。当程序运行时需要按需分配内存——比如那些可能比当前函数存活更久的对象，或者大小可以增长的集合——堆就是这些内存的来源。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.cpdd.fyi/images/heap-memory-deep-dive/stack-vs-heap.svg" alt="堆栈结构对比图"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;关键区别在于生命周期：&lt;/strong&gt; 栈变量在函数结束时消失，但堆对象可以比创建它的函数活得更久。&lt;/p&gt;</description></item><item><title>Redis 最容易踩的 7 个坑，问题往往不在 Redis</title><link>https://blog.cpdd.fyi/posts/redis-pitfalls-from-douyin/</link><pubDate>Sat, 04 Apr 2026 12:10:00 +0800</pubDate><guid>https://blog.cpdd.fyi/posts/redis-pitfalls-from-douyin/</guid><description>&lt;p&gt;很多人觉得 Redis 没什么门槛。&lt;/p&gt;
&lt;p&gt;命令不难，接入也快，压测时还总能打出很漂亮的数字。于是项目上线后，大家默认它“应该不会出事”。&lt;/p&gt;
&lt;p&gt;但我看过不少 Redis 问题，最后都不是 Redis 自己不行，而是前面的设计太随便：Key 怎么拆，TTL 怎么设，热点怎么扛，内存打满以后怎么办，这些事一开始没想清楚，后面都会用线上故障补课。&lt;/p&gt;
&lt;p&gt;我最近刷到一条讲 Redis 避坑的短视频，里面提到的点很典型。我没有照着转写，而是按“线上最容易出事故的顺序”重新整理成这篇文章。&lt;/p&gt;
&lt;p&gt;如果你只记一句话，那就是：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Redis 真正危险的坑，大多不是命令层面的，而是设计层面的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.cpdd.fyi/images/redis-pitfalls-cover.svg" alt="Redis 常见架构陷阱配图：中心是 Redis，周围是 Big Key、TTL、Hot、OOM 等风险信号"&gt;&lt;/p&gt;</description></item><item><title>2024年前端开发趋势预测</title><link>https://blog.cpdd.fyi/posts/web-development-trends/</link><pubDate>Sat, 20 Jan 2024 16:45:00 +0800</pubDate><guid>https://blog.cpdd.fyi/posts/web-development-trends/</guid><description>&lt;h2 id="-引言"&gt;🌟 引言&lt;/h2&gt;
&lt;p&gt;2024年对于前端开发者来说是一个充满机遇的年份。随着Web技术的快速发展，新的框架、工具和最佳实践不断涌现。本文将深入分析今年前端开发的主要趋势，帮助开发者把握技术发展方向。&lt;/p&gt;
&lt;h2 id="-框架生态的演进"&gt;🚀 框架生态的演进&lt;/h2&gt;
&lt;h3 id="react-18-的成熟应用"&gt;React 18+ 的成熟应用&lt;/h3&gt;
&lt;p&gt;React在2024年继续保持其主导地位，特别是以下特性的广泛应用：&lt;/p&gt;
&lt;h4 id="并发特性"&gt;并发特性&lt;/h4&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-jsx" data-lang="jsx"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;startTransition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useTransition&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;react&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;SearchApp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isPending&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;startTransition&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useTransition&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;searchQuery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setSearchQuery&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;searchResults&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setSearchResults&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleSearch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;setSearchQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;startTransition&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 非紧急更新，不会阻塞用户输入
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;setSearchResults&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;performExpensiveSearch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;SearchInput&lt;/span&gt; &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleSearch&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isPending&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;SearchSpinner&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;SearchResults&lt;/span&gt; &lt;span class="na"&gt;results&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;searchResults&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h4 id="suspense数据获取"&gt;Suspense数据获取&lt;/h4&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-jsx" data-lang="jsx"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;ProfilePage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;Suspense&lt;/span&gt; &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ProfileSkeleton&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;}&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;UserProfile&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;Suspense&lt;/span&gt; &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;PostsSkeleton&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;}&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;UserPosts&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;Suspense&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;Suspense&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h3 id="vue-3的组合式api生态"&gt;Vue 3的组合式API生态&lt;/h3&gt;
&lt;p&gt;Vue 3的组合式API已经成为主流开发方式：&lt;/p&gt;</description></item></channel></rss>