Chapter 10

性能优化与生态

深入细粒度更新原理、避开追踪陷阱、代码分割、测试与最终选型建议

1. 细粒度更新原理深讲

理解 SolidJS 的更新机制,才能写出真正高性能的代码。让我们深入了解 Signal 的内部工作方式。

订阅者图(Subscriber Graph)

SolidJS 在内存中维护一个有向图:Signal 是节点,Effect/Memo 是订阅者。每当 Signal 被读取时,当前的追踪上下文被添加到该 Signal 的订阅列表;当 Signal 被写入时,通知所有订阅者重新执行。

// 内部模型(伪代码)
class Signal {
  value: T;
  subscribers: Set<Effect> = new Set();

  read() {
    if (currentEffect) {
      this.subscribers.add(currentEffect);
      currentEffect.dependencies.add(this);
    }
    return this.value;
  }

  write(newValue: T) {
    if (this.value !== newValue) {
      this.value = newValue;
      this.subscribers.forEach(effect => effect.schedule());
    }
  }
}

// 每次 Effect 执行时:
// 1. 清除旧的依赖关系
// 2. 设置 currentEffect = this
// 3. 执行 fn(),期间所有 Signal 读取都会注册订阅
// 4. 恢复 currentEffect = null

为什么 SolidJS 不需要 useMemo 和 useCallback

React 需要 useMemo/useCallback 是因为每次 re-render 都会重新创建所有值和函数。SolidJS 的组件只运行一次,所以:

2. 避免响应式追踪陷阱

SolidJS 的追踪系统非常智能,但有几个常见陷阱需要注意:

陷阱一:解构 Signal 或 props

function BadComponent() {
  const [count, setCount] = createSignal(0);

  // ❌ 解构后 value 是普通数字,不是响应式的
  const { value } = { value: count() }; // 解构时就求值了
  return <p>{value}</p>; // value 永远是初始值 0
}

function GoodComponent() {
  const [count, setCount] = createSignal(0);

  // ✅ 在 JSX 中直接调用 Signal getter
  return <p>{count()}</p>;
}

陷阱二:在追踪上下文外读取 Signal

function TimerComponent() {
  const [count, setCount] = createSignal(0);

  setInterval(() => {
    // ❌ setTimeout/setInterval 回调不在追踪上下文中
    // 读取 count() 不会建立订阅,但写入 setCount 仍会触发更新
    console.log("current:", count());
    setCount(c => c + 1); // ✅ 写入仍然有效
  }, 1000);

  return <p>{count()}</p>; // ✅ JSX 中的读取是响应式的
}

陷阱三:提前求值

function Component(props: { items: string[] }) {
  // ❌ 在组件顶层(非响应式上下文)提前求值
  const length = props.items.length; // 只读取一次

  // ✅ 在 JSX 或 createMemo 中读取(建立追踪)
  const length = createMemo(() => props.items.length);
  return <p>共 {length()} 项</p>;
}

3. lazy() — 代码分割

lazy() 与 Suspense 配合实现组件级别的代码分割:

import { lazy } from "solid-js";
import { Suspense } from "solid-js";

// Vite 会将每个 lazy 模块打包成独立 chunk
const HeavyChart = lazy(() => import("./components/HeavyChart"));
const RichEditor = lazy(() => import("./components/RichEditor"));

function App() {
  const [tab, setTab] = createSignal("chart");

  return (
    <div>
      <button onClick={() => setTab("chart")}>图表</button>
      <button onClick={() => setTab("editor")}>编辑器</button>

      <Suspense fallback={<p>加载组件...</p>}>
        <Show when={tab() === "chart"}>
          <HeavyChart />
        </Show>
        <Show when={tab() === "editor"}>
          <RichEditor />
        </Show>
      </Suspense>
    </div>
  );
}

4. Web Components 支持

SolidJS 可以将组件编译为原生 Web Components(Custom Elements),方便在任何框架或纯 HTML 中使用:

import { customElement } from "solid-element";
// npm install solid-element

// 将 SolidJS 组件注册为 Custom Element
customElement(
  "my-counter",     // 自定义元素标签名(必须含连字符)
  { initialCount: 0 }, // 默认属性
  (props) => {
    const [count, setCount] = createSignal(props.initialCount);
    return (
      <div>
        <span>{count()}</span>
        <button onClick={() => setCount(c => c + 1)}>+</button>
      </div>
    );
  }
);

// 在任意 HTML 中使用
// <my-counter initial-count="5"></my-counter>

5. 测试:Vitest + @solidjs/testing-library

# 安装测试依赖
pnpm add -D vitest @solidjs/testing-library @testing-library/jest-dom jsdom
// vite.config.ts — 配置测试环境
import { defineConfig } from "vite";
import solid from "vite-plugin-solid";

export default defineConfig({
  plugins: [solid()],
  test: {
    environment: "jsdom",
    globals: true,
    setupFiles: ["./src/test-setup.ts"],
  },
});
// Counter.test.tsx — 组件测试示例
import { render, screen, fireEvent } from "@solidjs/testing-library";
import { describe, it, expect } from "vitest";
import Counter from "./Counter";

describe("Counter", () => {
  it("初始值为 0", () => {
    render(() => <Counter />);
    expect(screen.getByText("Count: 0")).toBeInTheDocument();
  });

  it("点击 + 按钮后增加", () => {
    render(() => <Counter />);
    fireEvent.click(screen.getByText("+"));
    expect(screen.getByText("Count: 1")).toBeInTheDocument();
  });

  it("支持自定义初始值", () => {
    render(() => <Counter initialCount={5} />);
    expect(screen.getByText("Count: 5")).toBeInTheDocument();
  });
});

6. SolidJS 生态概览

类别推荐库说明
路由@solidjs/router官方路由库
全栈框架SolidStart官方元框架(类 Next.js)
样式Tailwind CSS / vanilla-extract均支持 SolidJS
UI 组件库SUID(Material)/ Hope UI / Kobalte原生 SolidJS 组件库
表单@modular-forms/solid类型安全表单库
数据请求createResource(内置)/ solid-querytanstack query 的 Solid 版
动画solid-transition-group / motionCSS 过渡 / 物理动画
国际化@solid-primitives/i18n响应式 i18n
测试vitest + @solidjs/testing-library官方测试方案

7. 与 React 的最终选型建议

选择 SolidJS 当...

  • 性能是首要需求(游戏、实时数据、复杂交互)
  • 团队愿意投入学习细粒度响应式思维
  • 项目是新建的,没有历史包袱
  • 希望最小的运行时体积(~7KB)
  • 需要构建 Web Components
  • 团队已有 RxJS/MobX 背景

坚持 React 当...

  • 团队已有大量 React 经验和历史代码
  • 需要最大的生态系统(npm 包、UI 库、招聘)
  • 业务性能要求不极端(React 已经足够快)
  • 需要 React Native 跨平台
  • 团队规模大,统一技术栈更重要
  • 使用 Next.js 全栈功能
ℹ️

务实建议:SolidJS 在技术上确实更先进,性能更好,心智模型更简单(一旦理解了 Signal)。但 React 的生态和社区规模仍然远大于 SolidJS。

如果你是个人项目或新团队,SolidJS 是值得尝试的优秀选择。如果是大型商业项目,先评估团队学习曲线和生态成熟度再决定。

8. SolidJS vs React 概念对照表

ReactSolidJS关键差异
useStatecreateSignalSignal 是函数调用,不触发 re-render
useEffectcreateEffect自动追踪依赖,无需声明数组
useMemocreateMemocreateMemo 永远正确缓存,无依赖陷阱
useCallback普通函数不需要,组件只执行一次
useReflet ref! / createSignallet 变量即可(组件只跑一次)
useContextuseContextAPI 类似
Redux/ZustandcreateStore / Context内置 Store 更强大
array.map()<For>For 更高效,正确处理列表 diff
condition &&<Show>Show 正确处理清理
React.lazylazy()API 相同
Next.jsSolidStart类似设计,Server Functions 更简洁

课程总结:恭喜完成 SolidJS 全部 10 章的学习!你现在掌握了:细粒度响应式原理(Signal/Effect/Memo)、组件规范(不解构 props)、控制流组件(Show/For/Switch)、Store 状态管理、Context 依赖注入、SolidRouter 路由、异步资源管理、SolidStart 全栈开发,以及性能优化和测试。下一步,建议动手构建一个完整的 SolidStart 全栈应用来巩固所有知识!