Babel JSX Improvement

Build

在 React 的开发中,需要在 Component 的 render 函数或是 Functional Component 的函数中,返回一个定义好的 JSX 内容,用于表示具体需要渲染出来的 UI 样式。Babel 或 TypeScript 会在编译时将这个对象转化成一个 JavaScript 可以理解的一般函数调用(具体调用的函数根据库的不同可能存在差异,对于 React 来说就是 React.createElement 函数,对于 Preact 来说则是 h 函数)。这个函数会在运行时被执行,并返回一个普通的 Object。React 拿到这个 Object 之后,就可以根据其中的内容来渲染出对应的 UI(根据具体执行的环境,这个步骤可能通过 React-DOM 或 React Native 来完成)。

既然 JSX 的部分会被编译成普通的函数调用,并在运行时被反复执行,这里必然会有一些性能上的损耗。

而在实际的开发中,存在着很多的组件,实际需要返回的 JSX 是固定不变的。比如说:

const LoadableButton = ({ loading, ...rest }) => {
  if (loading) return <Loading />;
  return <Button {...rest} />;
}

在上面这个例子中,实际上条件的第一种结果,返回的 JSX 是一个固定的值。手动的优化可以这么写:

const loadingComponent = <Loading />;
const LoadableButton = ({ loading, ...rest }) => {
  if (loading) return loadingComponent;
  return <Button {...rest} />;
}

这样,每次当 loading = true 的时候,都会直接返回 loadingComponent,而不需要反复执行 React.createElement(Loading) 这个函数去拿到最终的返回 Object。除了在运行时减少了重复计算,节省了时间和内存开销(这个在 re-render 非常频繁的时候有一定的优势),另一个好处是,React 可以通过比较返回的结果知道 Object 并没有发生变化,从而直接结束渲染的流程,不再进行接下来更深层次的渲染。

这部分的操作,其实可以交给编译器去完成。Babel 有一个插件 @babel/plugin-transform-react-constant-elements 可以拿来做这方面的优化,具体的使用方式以及可能存在的问题可以参考文档

当然,上面的优化依然存在小的瑕疵:因为把创建 Object 的操作提到了初始化的时候就直接进行了,如果存在大量类似的优化,会导致 JavaScript 初始运行的速度被减慢。大量的 Object 被事先创建了出来,而实际上这部分内容都远还没有到需要用的时候。一个更极致的优化可以这么写:

let loadingComponent;
const LoadableButton = ({ loading, ...rest }) => {
  if (loading) {
    if (!loadingComponent) loadingComponent = <Loading />;
    return loadingComponent;
  }
  return <Button {...rest} />;
}

这部分的操作就不是 Babel 插件原生支持的了。

从目前的实际情况来看,生成 JSX 对应 Object 拖慢初始化的例子暂时还不存在(毕竟 React.createElement 的执行速度并不是非常慢,而且一个项目中的 JSX 数量也不会非常庞大)。如果有必要,可以 GitHub 上提出 PR,按照类似上面提到的方式进行进一步的优化。