本节摘要:同样一段 JSX,React 编译成"每次渲染都执行的元素工厂调用",Solid 编译成"一份 HTML 模板加少量细粒度绑定"。本节把两份产物并排摊开,解释模板克隆为什么是首屏快的关键,并清点书写层语法差异——
class、onInput、style 键名——构成一张迁移期排雷清单。
「模板克隆」是理解 Solid 性能的钥匙词汇。它指的是:编译器把你 JSX 里的静态结构(标签层级、类名、绝大多数属性)预先做成一份真实的 DOM 模板,渲染时用一次克隆批量复制;只有 JSX 表达式位置(插值、事件、动态属性)被单独抽出来,挂成响应式绑定。React 的编译产物没有这个环节——它生成的是创建元素描述对象的函数调用,每次渲染都重新执行、重新生成一棵描述树。
先写源码,一个带标题和计数的卡片:
function Card(props) { return ( <div class="card"> <h2 class="card-title">{props.title}</h2> <p class="card-desc">固定文案,永远不变</p> <button class="btn" onClick={props.onLike}> 赞 {props.count()} </button> </div> ); }
React 的编译产物(简化示意):
// 每次渲染都执行,生成一棵全新的对象描述树 function Card(props) { return jsx("div", { className: "card", children: [ jsx("h2", { className: "card-title", children: props.title }), jsx("p", { className: "card-desc", children: "固定文案,永远不变" }), jsx("button", { className: "btn", onClick: props.onLike, children: ["赞 ", props.count()] }) ] }); }
Solid 的编译产物(简化示意):
// 组件体执行一次:克隆模板 + 挂绑定 const _tpl = template(`<div class="card"><h2 class="card-title"></h2><p class="card-desc">固定文案,永远不变</p><button class="btn"></button></div>`); function Card(props) { const _el = _tpl.cloneNode(true); // 结构来自克隆,不是重建 insert(_el, () => props.title, _el.firstChild); // 文本绑定 onClick(_el.lastChild, props.onLike); // 事件委托 insert(_el.lastChild, () => ["赞 ", props.count()]); return _el; }
两份产物对照读,三个信息量最大的差异就浮出来了。差异一:静态内容的位置。"固定文案,永远不变"在 React 里是每次渲染都要传递的字符串实参,在 Solid 里它躺在模板字符串里,克隆之后根本不进任何执行路径——静态内容从此零成本。差异二:更新的组织方式。React 靠"新描述对旧描述"的 diff 找出变化,Solid 的每个表达式位置都换成了() =>包裹的取值函数,信号变化时这些函数重新执行、直接写入节点,对账环节不存在。差异三:事件处理。Solid 编译器把事件统一挂到文档根做委托,组件里的 onClick 不会成为 DOM 节点上的独立监听器——大量列表条目的事件内存开销因此骤降。

编译目标不同,书写层的方言差异必须背下来,否则迁移期会撞上一堆"看起来一样却行为不同"的坑:
| 书写点 | React 习惯 | Solid 写法 | 说明 |
|---|---|---|---|
| 类名 | className |
class |
编译目标就是真实 DOM 属性 |
| 输入框 | onChange |
onInput |
Solid 沿用原生事件名,onChange 不存在 |
| style | 驼峰键名对象 | 驼峰键名对象 | 一致,但建议配合 classList 指令 |
| 危险 HTML | dangerouslySetInnerHTML |
innerHTML 属性 |
命名直白,别滥用 |
| SVG 属性 | 驼峰转换 | 与 DOM 一致 | 如 stroke-width 直接写 |
| 事件对象 | 合成事件 e.target.value |
原生事件 e.currentTarget.value |
没有事件池与合成层 |
表里最容易踩的两格加粗提醒:onChange 在 Solid 里根本不存在,输入框必须用 onInput;事件对象是浏览器原生对象,取值用 e.currentTarget——React 的 e.target 习惯在事件委托下会拿到错误的节点(委托后 target 可能是内部子元素)。这两条能排掉迁移期一半的诡异 bug。
⚠️
class写成className不会报错,但会被当成一个叫 className 的未知属性静默丢弃,样式不生效且难排查。团队迁移前把这条写进代码评审清单,性价比极高。
语法差异清单之外,还有一个"组件本身是变量"的场景值得单独说。React 里直接把组件存进变量再当标签用(const Tag = props.kind === "a" ? A : B; <Tag />),因为渲染就是执行,变量自然可用。Solid 里标签位是编译期静态分析的边界,动态组件走专门的 Dynamic 组件:
import { Dynamic } from "solid-js/web"; <Dynamic component={props.kind === "a" ? A : B} size="lg" /> // 组件变量变化时,旧组件销毁、新组件装载,作用域随之切换
配套地,"往子组件里塞内容"的插槽场景在两边形态也不同:React 的 props.children 是求值后的元素树,Solid 的 props.children 是可求值多次的响应式结构——需要把它当函数调用(配合工具函数)或直接渲染。这一差异是 render props 模式在 5.3 节"变形"的物理基础,先在语法层记住:Solid 的 children 是活的,React 的 children 是快照。
能,而且推荐。开发构建的产物与源码在同一工程内可对照查看,编译产物的可读性是 Solid 的一个隐性优点:你在第 3 章看到的模板克隆与绑定代码就是真实产物的形状,不是教学示意。调试"为什么这里不更新"时,看一眼产物里这个表达式位有没有生成绑定函数,往往比改代码碰运气快得多。7.3 的学习路径里,"读一次自己项目的编译产物"被列为第一周的作业,理由正在于此。
下一章视角从编译期转回运行期:组件装载之后的生命周期,在"只跑一次"的世界里还剩下什么。