
异步 UI 最让人头疼的,从来不是“慢”,而是那种来回的“抖动”。
接口一慢,页面就会开始抽风:这块先空一会儿、那块突然闪一下、你刚点完按钮,它又把整块 UI 给重置了。到最后,项目里会塞满各种 loading、isFetching、isMutating、isRefetching 的状态。看起来做得很细致,但实际上全靠团队成员自觉别写错逻辑。而在多人协作的环境里,依赖“自觉”这种东西,嗯……多少有点奢侈。
Solid 2.0 进入 Beta 阶段,我觉得最有价值的不是“又增加了几个新 API”,而是它把构建异步 UI 的复杂问题,清晰地拆解成了三件相互独立的事情:
- 首屏:第一次到底能不能把 UI 画出来?
- 刷新:UI 已经画出来了,后台在更新数据时,要不要给提示、怎么给提示?
- 写入:用户点击提交、保存、点赞这类会改变数据的操作时,整个流程应该如何收拢?
你未必会使用 Solid,但这三件事在任何前端框架里都躲不掉。把它们的概念拆开、讲清楚,很多让用户体验起来“像坏掉了一样”的问题,解决思路会立刻变得清晰。
(说明:下面的代码示例是概念性写法,具体的 API 名称和细节请以 Solid 2.0 的官方文档为准。我更想传递的,是关于问题分工和设计心智模型的思考。)
1)首屏:先回答“能不能画出来”

首屏要解决的问题很朴素:在数据还没获取到之前,这块 UI 能不能被渲染出来?
如果不能,那你就需要一个“首屏就绪边界”:当某部分子树暂时无法渲染时,先展示一个 fallback(占位符);一旦内容准备就绪,就稳定地替换上去。这里的重点是“稳定下来”,不要把它理解成“每次刷新数据时,都把整个 UI 替换成加载动画”。
概念上,它类似于这样的结构:
<Loading fallback={<Spinner />}>
<UserList />
</Loading>
我见过最常见的“翻车”做法是:把“首屏加载”和“后台刷新”混成一个 isLoading 状态,然后每次 revalidate(重新验证)时,都把列表整个卸载、再重建一遍。用户的体感就是页面在不停“眨眼”,而开发者还觉得自己“很贴心地做了 loading 状态”。(对,你是做了,但做得让人很烦。)
所以,处理首屏这件事的关键词是:就绪(Readiness),而不是“忙不忙(Busy)”。
2)刷新:UI 别“抖”才是重点

首屏问题解决了,日常的“折磨”才刚刚开始:UI 已经显示出来了,但数据正在后台刷新。
这时你真正需要的,是一个“刷新中”的提示,而不是把现有 UI 砸掉、重画一遍。很多产品体验做得舒服,靠的就是这套策略:先稳定地展示旧数据,后台悄悄更新,更新完成后再无缝替换。
Solid 2.0 的思路,更像是把“刷新”这个概念从一堆零散的布尔标志(flags)里解放出来:不要再到处传递 isFetching 了,而是把“与这个表达式相关的工作是否正在进行中(pending)”变成一种可以被查询的状态。
概念写法大概是这样:
const refreshing = () => isPending(() => users());
<Show when={refreshing()}>
<span class="hint">刷新中…</span>
</Show>
<Loading fallback={<Spinner />}>
<UserList users={users()} />
</Loading>
这里的职责分工我很喜欢:
- 首屏就绪:交给
<Loading> 边界。
- 后台刷新:交给
isPending 等原语来查询。
这比纠结“我到底该用 isLoading 还是 isFetching”要靠谱得多。因为你的 UI 需求本质上就两类:第一次加载时别空着,后续更新时别乱抖。
3)写入(Mutation):流程要集中收拢

真正让代码变得零散、难以维护的,往往不是读取数据,而是写入数据(Mutation):
- 点击“新增”:要不要先做乐观更新(Optimistic Update)?
- 操作失败:该如何回滚、如何提示用户?
- 操作成功:要不要触发 refresh、这个刷新逻辑该放在哪里调用?
- 遇到并发提交:以哪一次的操作为准?
很多项目写到最后就会变成这样:组件里一堆 setState 或本地状态更新,外面再裹一层数据失效(invalidation)逻辑,中间还夹着 try/catch 错误处理。如果你让一个新同学去读一遍“点下这个按钮后到底会发生什么”,他能完整理清算我输。
Solid 2.0 为 Mutation 提供了一个更明确的归宿:把一次写入操作的生命周期收拢成一个“动作(Action)”,再配合乐观更新的原语,让整个流程能够按照时间顺序写在一起。
概念上像这样:
const addTodo = action(async (todo) => {
optimisticPush(todo); // UI 先乐观地更新一下
await api.addTodo(todo); // 执行真实的异步写入
refreshTodos(); // 确保读取端的数据对齐
});
你可以不喜欢它具体的语法,但这个“写成一段连续流程”的约束非常有价值:它强迫开发者必须把状态变化的先后顺序讲清楚。
(顺便吐槽一句:很多 Mutation 代码让人心烦,不是因为它本身逻辑复杂,而是因为它被分散在三个文件、五个 Hook、两套状态管理体系里。你每次改需求都要像侦探一样追着线索跑半天,最后发现坑还是自己当初挖的。)
4)调度:不要依赖“玄学”和巧合
如果你写过响应式系统(或者被响应式系统坑过),就会知道“调度策略”一旦改动,用户体验的差异会非常明显:
- 什么时候进行批量更新(Batch)?
- 什么时候强制刷新(Flush)视图?
set 完状态后立刻读取,读到的是新值还是旧值?
像 Solid 2.0 这类在框架层进行的调度调整,往往不是为了“为难开发者”,而是为了让异步、并发场景下的行为变得更可预测。如果你一直依赖“碰巧在同一个事件循环Tick里读到了新值”这种巧合,那么项目迟早会在某次引入 Transition 或异步渲染之后,出现各种难以调试的“鬼打墙”问题。
我自己会这样处理这类变化:
- 先找出项目中“同步读写依赖”最强的地方(例如,
set 状态后立刻读取、立刻计算衍生值、立刻发送新请求)。
- 要么把它们改成显式的
flush / await,要么把读取逻辑放到正确的 effect 或任务队列里。
- 不要试图强行抹平框架行为差异(硬抹平通常只会把问题推迟到线上爆发)。
5)重新分类异步 UI 问题
即便你完全不打算接触 Solid 2.0,我也建议你先把异步 UI 的麻烦事,用以下三类来框定:
- 首屏:核心是 Readiness(能否渲染)。
- 刷新:核心是 Pending(后台刷新中,但 UI 保持稳定)。
- 写入:核心是 Action / Mutation(流程集中收拢)。
你会发现,很多关于“我到底该加哪个 loading”的团队争论,会立刻转化为可以对齐的工程问题:我们现在处理的是哪一类?我们要实现的目标是“占位”、“提示”还是“一个完整的事务流程”?
至于要不要迁移到 Solid 2.0 Beta——别急着全局替换,真的别。否则,你的周五晚上可能会过得非常“精彩”。(我说的是那种“精彩到让你想立刻撤回刚才那句 git commit”的体验。)
你对这种梳理异步逻辑的心智模型有什么看法?欢迎在云栈社区的前端框架板块与其他开发者交流讨论。无论是 React、Vue.js 还是 Solid,理清数据流与UI更新的边界,都是提升应用体验的关键。