上个季度,我们有一个客户项目,给了我们完美的借口来尝试一件我们一直想做的事情:用 SolidJS 和 React 构建同一个应用程序,然后比较一切。不是待办事项应用。不是计数器。而是一个真实的生产仪表板,包括身份验证、实时数据更新、复杂表单和一个渲染数千个数据点的图表模块。

结果让我们感到惊讶。不是因为某个框架「赢了」——而是因为权衡远比 Twitter 上的讨论复杂得多。有些事情符合我们的预期。有些事情完全出乎意料。这就是我们真正学到的东西。

目录

我们用 SolidJS 和 React 构建了同一个应用:Signals vs Hooks

我们构建的应用

该应用程序是一个电子商务客户的实时分析仪表板。其中包括:

  • 身份验证流程,包括 JWT 令牌和刷新逻辑
  • 包含 6 个小部件面板的仪表板,每个面板从不同的 API 端点拉取数据
  • 实时订单流,使用 WebSocket 连接
  • 交互式图表,渲染 5,000 多个数据点(使用图表库)
  • 复杂筛选表单,包括依赖下拉菜单和日期范围选择器
  • 管理设置面板,具有嵌套状态管理
  • 响应式布局,带有侧边栏导航

两个版本连接到相同的后端 API。两者都使用 TypeScript。两者都使用 Vite 作为构建工具。我们尽可能地保持第三方依赖相似——相同的图表库 (Chart.js)、相同的 HTTP 客户端 (ky)、相同的 WebSocket 包装器。

React 版本使用 React 19 和 hooks。SolidJS 版本使用 Solid 1.9。我们没有使用任何元框架(没有 Next.js,没有 SolidStart),因为我们想隔离框架差异,而不让路由和 SSR 影响比较。

Signals vs Hooks:心智模型的转变

这是重中之重。老实说,我们的团队花了大约一周的时间才停止在 SolidJS 中写 React 风格的代码。

React Hooks 如何工作

你可能已经知道了,但为了比较,值得明确说明。React 的模型是:当状态改变时,组件函数重新执行。整个函数。每个 useState、每个 useMemo、每个 useCallback——它们都是绕过整个函数重新运行这一事实的机制。

// React:每次状态改变时,整个函数都会重新运行
function OrderFeed() {
  const [orders, setOrders] = useState([]);
  const [filter, setFilter] = useState('all');
  
  // 除非我们使用 memoize,否则每次渲染都会重新计算
  const filteredOrders = useMemo(() => 
    orders.filter(o => filter === 'all' || o.status === filter),
    [orders, filter]
  );

  // 这个 ref 在概念上每次渲染都会重新创建
  const handleNewOrder = useCallback((order) => {
    setOrders(prev => [order, ...prev]);
  }, []);

  useEffect(() => {
    const ws = connectWebSocket(handleNewOrder);
    return () => ws.close();
  }, [handleNewOrder]);

  return <OrderList orders={filteredOrders} />;
}

Solid Signals 如何工作

Solid 的模型从根本上不同。组件函数只运行 一次。之后,只有读取 signal 的特定表达式在该 signal 改变时重新执行。没有虚拟 DOM 差异。没有协调。响应式图追踪哪些 DOM 节点依赖于哪些 signals。

// Solid:这个函数只运行一次
function OrderFeed() {
  const [orders, setOrders] = createSignal([]);
  const [filter, setFilter] = createSignal('all');
  
  // 这会自动被追踪——不需要依赖数组
  const filteredOrders = createMemo(() => 
    orders().filter(o => filter() === 'all' || o.status === filter())
  );

  // 不需要 useCallback——这个闭包是稳定的
  const handleNewOrder = (order) => {
    setOrders(prev => [order, ...prev]);
  };

  // onMount 而不是带空依赖的 useEffect
  onMount(() => {
    const ws = connectWebSocket(handleNewOrder);
    onCleanup(() => ws.close());
  });

  return <OrderList orders={filteredOrders()} />;
}

这在实践中意味着什么

Solid 版本没有依赖数组。没有 useCallback。没有派生状态的 useMemo(尽管 createMemo 存在用于昂贵的计算——区别在于它是性能的可选项,而不是正确性的必需项)。

以下是开发过程中实际困扰我们的内容:

  1. 在 Solid 中解构 props 会破坏响应性。 我们有一个初级开发人员在 Solid 组件中解构 props,我们花了 45 分钟调试为什么更新没有传播。在 React 中,解构很好,因为整个函数重新运行。在 Solid 中,你需要在 JSX 或追踪的作用域内访问 props.value

  2. 早期返回在 Solid 中很棘手。 你不能在 Solid 组件的顶部执行 if (!data()) return <Loading />,就像在 React 中那样。组件函数只运行一次,所以那个条件永远不会重新评估。你需要改用 <Show when={data()}>

  3. 没有陈旧闭包的 bug。 这是一个大胜利。在我们的 React 版本中,第一周我们有三个单独的陈旧闭包 bug,与捕获旧状态的 WebSocket 处理程序相关。Solid 版本有零个,因为 signals 在访问时总是被读取,而不是在闭包中捕获。

Bundle 大小对比

我们使用完全相同的 Vite 配置、gzip 压缩和代码拆分策略来测量两个生产构建。

指标 React 19 SolidJS 1.9 差异
框架运行时 44.2 KB (gzip) 7.1 KB (gzip) -84%
初始 bundle 总大小 127.8 KB 89.3 KB -30%
总应用(所有块) 312.4 KB 274.1 KB -12%
第一个块(关键路径) 68.4 KB 41.2 KB -40%
块数 14 14 相同

Solid 的运行时明显更小。这不是新闻——Ryan Carniato 已经广泛讨论过这个问题。但让我们惊讶的是,一旦添加真实应用代码、第三方库和资产,总应用大小的差异会显著缩小。

框架运行时对初始加载最重要。7.1 KB gzipped 的 Solid 基本上消失在噪音中。React 的 44 KB 是显而易见的,尤其是在移动连接上。

Tree Shaking

Solid 的 tree shaking 效果比 React 更好。未使用的 Solid 原始类型会完全被消除。对于 React,协调器和 fiber 架构无论你是否使用所有功能都会被发送。我们通过在两者中构建最小页面来确认这一点——Solid 的下限更低。

我们用 SolidJS 和 React 构建了同一个应用:Signals vs Hooks - 架构

渲染性能基准测试

我们使用 Chrome DevTools 性能配置、Lighthouse 和自定义计时工具运行了结构化基准测试。所有测试在 M2 MacBook Pro 上进行,CPU 限流设置为 4 倍,以模拟中端设备。

图表渲染(5,000 个数据点)

指标 React 19 SolidJS 1.9
初始渲染 142ms 89ms
筛选更改时重新渲染 67ms 23ms
渲染期间的内存 18.4 MB 11.2 MB
创建的 DOM 节点 5,847 5,812

Solid 在重新渲染上始终更快,因为它不对虚拟 DOM 树进行差异化。当筛选 signal 改变时,只有读取该 signal 的表达式会更新。React 19 的编译器改进有所帮助——相同的测试在 React 18 上重新渲染时为 95ms——但 Solid 仍有明确的优势。

实时订单流(100 次更新/秒)

这是事情变得真正有趣的地方。我们每秒向订单流推送 100 条 WebSocket 消息。

指标 React 19 SolidJS 1.9
掉帧(每 10 秒) 12 2
平均绘制时间 8.3ms 3.1ms
最大绘制时间 34ms 11ms
CPU 使用率(平均) 24% 9%

Solid 在这个基准测试中绝对胜利。高频更新是细粒度响应式的最大收益点。React 正在批处理更新(这很好),但在每个批次中仍在对比更多的 DOM。

我们应该注意:React 19 的 useTransition 和自动批处理使这比 React 18 好得多。而且对于大多数真实世界的应用,你不会每秒推送 100 次更新。以每秒 10 次更新的速度,两个框架都很平滑。

包含 40 个字段的复杂表单

指标 React 19 SolidJS 1.9
击键输入延迟 2-4ms <1ms
表单提交渲染 28ms 12ms
每次击键的组件重新渲染 3-8 1

在 React 中,在一个字段中键入会导致父组件重新渲染,除非我们仔细 memoize 一切。在 Solid 中,在字段中键入字面上只更新该字段的 DOM 文本节点。其他都不会重新执行。

开发者体验和生态系统

性能不是一切。你必须实际构建这个东西、调试它、为其招聘和维护它。

生态系统规模(截至 2025 年初)

因素 React SolidJS
npm 周下载量 ~28M ~85K
GitHub 星标 233K+ 33K+
Stack Overflow 问题 470K+ ~2K
UI 组件库 50+ 成熟选项 5-8 选项
职位发布(LinkedIn 美国) ~45,000 ~200
元框架 Next.js(成熟) SolidStart(测试版)

这是房间里的大象。React 的生态系统大得不成比例。当我们需要为 Solid 编写日期范围选择器时,我们必须移植一个 React 的或自己编写。在 React 中,我们有 15 个选项可供选择。

我们在 Solid 中使用 Kobalte 来处理可访问的 UI 原始类型,这确实很好。但在组件覆盖方面,它不如 Radix UI。

调试

React DevTools 成熟、维护良好,是每个前端开发人员都知道的。Solid 有自己的 DevTools 扩展,还不错——你可以检查 signal 图,这对于追踪响应式 bug 实际上比 React 的组件树视图更有用。但它不够完善。

TypeScript 支持

两者都很优秀。Solid 对 JSX 的 TypeScript 支持实际上稍微更好,因为编译器在构建时处理更多。我们在 Solid 代码库中减少了类型体操。

真正重要的生产权衡

在构建两个版本并将 Solid 版本部署到暂存环境后(客户最终选择了 React 生产——详见下文),以下是真正重要的权衡:

招聘

我们的客户有一个 8 人的前端开发团队。都知道 React。没有人使用过 Solid。培训时间估计:2-3 周达到熟练程度,2-3 个月达到精通。这是一个真实的成本。这是大多数生产决策中的单一最大因素,也是为什么我们通常为需要频繁招聘的团队推荐 React 或像 Next.js 这样的框架。

第三方库兼容性

我们必须为三个具有 React 特定集成的库编写自定义包装器。这增加了大约 20 小时的开发时间。在 React 项目中,那些小时根本不存在。

服务端渲染

SolidStart 前景光明,但在我们的项目期间仍在测试版。Next.js 经过实战检验。对于需要生产级 SSR 和所有功能的项目,我们仍在使用 Next.js 开发或根据内容模型选择 Astro

真正重要的性能

这是老实话:对于这个特定的仪表板,两个框架的生产性能都足够好。Solid 版本在测量上更快,但 React 版本从来不是。用户不会注意到大多数交互的差异。

例外是高更新频率的实时流。如果你的应用有这样的用例,Solid 的性能优势是有意义的,而不仅仅是基准吹嘘。

何时选择 SolidJS 而非 React

在这个实验之后,这是我们对这一决定的诚实框架:

选择 SolidJS 当:

  • 你的应用有高频响应式更新(仪表板、交易平台、实时协作)
  • Bundle 大小很关键(嵌入式小部件、微前端、移动优先)
  • 你的团队很小,愿意投资学习新范例
  • 你想要细粒度响应式而不用与框架对抗
  • 你正在构建新东西,不需要大规模组件库生态

选择 React 当:

  • 你的团队已经知道 React(仅这一点通常就是决定性的)
  • 你需要成熟的元框架(Next.js、Remix)
  • 第三方库可用性很重要
  • 你在招聘,需要大量的人才库
  • 你的应用的性能要求是典型的(不极端)

对于我们的无头 CMS 开发项目,React 仍然主导,因为 CMS 集成(Sanity、Contentful、Storyblok)拥有一流的 React SDK。Solid 支持存在但通常是社区维护的。

代码对比:真实模式

让我们看一些项目中的真实模式并排比较。

带有加载状态的数据获取

React:

function Dashboard() {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchDashboardData()
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false));
  }, []);

  if (loading) return <Skeleton />;
  if (error) return <ErrorBanner error={error} />;
  return <DashboardGrid data={data} />;
}

SolidJS:

function Dashboard() {
  const [data] = createResource(fetchDashboardData);

  return (
    <Suspense fallback={<Skeleton />}>
      <ErrorBoundary fallback={(err) => <ErrorBanner error={err} />}>
        <DashboardGrid data={data()} />
      </ErrorBoundary>
    </Suspense>
  );
}

Solid 的 createResource 配合 Suspense 非常优雅。React 也有 Suspense(而且它在 React 19 中变得更好了),但 Solid 的版本感觉更自然。更少的样板,更少的状态变量来管理。

条件渲染

React:

{user ? (
  <Profile user={user} />
) : (
  <LoginPrompt />
)}

SolidJS:

<Show when={user()} fallback={<LoginPrompt />}>
  {(u) => <Profile user={u()} />}
</Show>

Solid 的 <Show> 组件存在是因为当组件函数只运行一次时,三元组不会以相同的方式工作。它需要习惯,但回调模式给你一个缩小的、非空的引用,这对 TypeScript 很好。

列表渲染

React:

{orders.map(order => (
  <OrderRow key={order.id} order={order} />
))}

SolidJS:

<For each={orders()}>
  {(order) => <OrderRow order={order} />}
</For>

Solid 的 <For> 不需要 keys,因为它通过引用追踪数组项。当项在数组中移动时,Solid 会移动实际的 DOM 节点。React 会卸载并重新挂载。这就是 Solid 列表性能如此出色的原因。

常见问题

SolidJS 在 2025 年生产就绪吗? yes。Solid 1.x 已经稳定超过两年。Cloudflare 和三星等公司已在生产中使用它。核心库成熟且经过充分测试。它周围的生态系统(SolidStart、组件库)比 React 的更小,但在稳步增长。问题不是 Solid 是否就绪——而是你的团队和项目要求是否与其生态系统大小一致。

我能从 React 逐步迁移到 SolidJS 吗? 不容易。尽管 JSX 语法相似,但运行时模型从根本上是不同的。你不能在 React 应用内嵌入 Solid 组件(反之亦然),除非没有 iframe 或 web 组件边界。迁移基本上是一次重写。如果你在考虑,从新的隔离功能或微前端开始,而不是尝试转换现有的 React 代码。

SolidJS 支持服务端渲染吗? 是的。SolidStart 提供 SSR、流式传输和服务器函数。它功能齐全且在快速改进,但截至 2025 年初,它仍然不如 Next.js 或 Remix 成熟。对于 SSR 繁重的项目,我们仍会根据你的内容需求推荐评估 Next.jsAstro

React 19 的编译器与 Solid 的方法相比如何? React 19 的编译器(前身是 React Forget)自动 memoize 组件以减少不必要的重新渲染。这是一个重大改进。但它仍然在 React 的模型范围内工作,即重新运行组件函数并对虚拟 DOM 进行差异化。Solid 完全跳过了这两个步骤。编译器使 React 更快,但不会改变基本架构。在我们的基准测试中,带编译器的 React 19 比 React 18 快约 30%,但在更新繁重的场景中仍然比 Solid 慢。

Preact Signals 作为中间地带怎么样? Preact Signals(和 @preact/signals-react 适配器)为 React 生态系统带来了类似 signal 的响应性。这是一种有趣的方法,但它是在与 React 的核心模型对抗,而不是与之配合。我们进行了短暂的测试,发现 Suspense 和并发功能的边界情况。如果你想要 signals,Solid 在没有阻抗不匹配的情况下为你提供完整体验。

SolidJS 比 React 更难学吗? 如果你已经知道 React,Solid 的 API 会看起来很熟悉——相似的 JSX、相似的模式。困难的部分是忘记 React 的心智模型。你会寻求 useEffect 模式,而这些模式不存在。你会解构 props 并破坏响应性。你会尝试早期返回,想知道为什么它们不起作用。对于有经验的 React 开发人员来说,预计花费 1-2 周时间在 Solid 中变得高效,再花 1-2 个月时间停止写 React 风格的 Solid。

哪个框架对 TypeScript 的支持更好? 两者都有出色的 TypeScript 支持。Solid 有轻微的优势,因为它的编译器可以在某些模式中提供更窄的类型(例如 <Show> 中的回调),而且你不需要复杂 React hooks 有时需要的大量泛型类型参数。但老实说,两者都很好。这不应该是决定因素。

我应该在下一个项目中使用 SolidJS 吗? 这取决于你的限制。如果你是一个小团队构建对性能敏感的东西,并且你愿意投资于学习曲线并围绕更小的生态系统工作,Solid 是一个真正优秀的选择。如果你需要招聘 React 开发人员、使用已建立的组件库或需要生产级元框架,React 是更安全的赌注。没有通用的答案——只有适合你具体情况的正确答案。如果你想讨论你项目的决定,联系我们,我们可以帮助你评估权衡。