React19からJSXの変換処理が高速に
Intro
Fast JSX: Don't clone props object #28768というPRが少し前にマージされました。これはReact19からjsxが高速になると言うPRです。PR内の説明を見ればなんとなくわかるかと思いますが、自分の理解のためブログとして残しておきます。
これまでのReactのjsx
今回の高速化に当たって、propsをクローンしなくなったことがポイントとしてあるようです。
ではそもそもなぜpropsをクローンしていたのでしょうか。これには1. key, refの予約語をpropsから削除すること / 2. createElementがpublic apiであるという二つのポイントがあります。
1. key, refの予約語をpropsから削除すること
Reactではprops.key
やprops.ref
でこの二つのプロパティにアクセスすることはできないです。しかし、コンポーネントに渡すことはできます。
// key
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
<li key={number.toString()} ref>
{number}
</li>
);
// ref
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));
keyはリスト内の要素を一意に識別するためのプロパティで、refはコンポーネントのdom動作を行うことができるプロパティです。
先ほど、refはprops.ref
でアクセスできないと言いましたが、React19からはprops.ref
でアクセスできるようになります。今まではReact.forwardRef
で囲む必要があり面倒だったので、Reactを使う人にとっては嬉しいアップデートです。
このアップデートによりrefの問題は解消できたので、残りはkeyになりました。そしてkeyも元々スプレット構文を使わなければクローンはされないようになっていたので、そのケース以外はクローンが必要ないことになりました。
2. createElementがpublic apiである
次にjsxは開発者はhtmlのように書くと思いますが、もちろんそのままではブラウザでは配信できないので、トランスパイルされます。その際にReact17からjsx
という関数を使いますが、それより以前はcreateElement
という関数にトランスパイルされていました。
ここら辺はReact17におけるJSXの新しい変換を理解するというブログがとてもわかりやすいです。
そしてjsx関数はreact/jsx-runtimeから、createElementはreactからimportして我々開発者も使用することができます。
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
const Foo = () => {
return _jsxs(
"div",
{
children: [
_jsx("p", { id: "a", children: "I am foo" }, void 0),
_jsx("p", { children: "I am foo2" }, "b"),
],
},
void 0,
);
};
// ↑ の処理は以下のコンポーネントをトランスパイルした結果
const Foo = () => {
return (
<div>
<p id="a">I am foo</p>
<p key="b">I am foo2</p>
</div>
);
};
ここで少し謎なのが、PRでは「the new JSX runtime, jsx
, is not a public API」と書いてあります。そのため使用できないのかと思いきや普通に使えたし警告も特になかったです。
ただコードジャンプしてみにいくと、「You should not use this function directly. Use JSX and a transpiler instead.」と書いてはありました。なので使えないではなく使ってもこっちは知らないよみたいなニュアンスなのかなと感じています。
その一方、createElement
は使われていることも考慮されているみたいですね。
整理すると、createElementはユーザーも使えるもので、これを使ってpropsの変更などがあった場合整合性が合わずバグになることもあるので、それを防ぐためにpropsをクローンして元のpropsを変更しないようにしていましたが、新しいjsx
はコンパイラとして内部的に使用されることを意図しているため、ユーザーによるpropsの変更を考慮する必要がなくなったと言う感じだと思っています。
実装からみる高速化の仕組み
高速化のために大規模な変更があったわけではなく、実装はとてもシンプルでした。
// ref: https://github.com/facebook/react/blob/ea26e38e33bffeba1ecc42688d7e8cd7e0da1c02/packages/shared/ReactFeatureFlags.js#L179-L182
export const enableRefAsProp = true;
export const disableStringRefs = true;
// ref: https://github.com/facebook/react/blob/ea26e38e33bffeba1ecc42688d7e8cd7e0da1c02/packages/react/src/jsx/ReactJSXElement.js#L348-L378
// after
let props;
if (enableRefAsProp && disableStringRefs && !('key' in config)) {
props = config;
// before
for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
// Skip over reserved prop names
propName !== 'key' &&
(enableRefAsProp || propName !== 'ref')
) {
if (enableRefAsProp && !disableStringRefs && propName === 'ref') {
props.ref = coerceStringRef(
config[propName],
ReactCurrentOwner.current,
type,
);
} else {
props[propName] = config[propName];
↑ をみると、keyがconfigにない場合(keyがconfigに渡される時はスプレット構文で渡された時)にconfigをそのままpropsとして渡してあげています。そしてそれをこの後にあるReactElementに渡してあげています。
beforeでは毎回configをfor分で回してpropsの名前をチェックしたりしているので、単純にfor分で回すかそのまま代入するかで速度に差がありそうだなということがわかります。
まとめ
今回はReact19でjsxが高速になると言う話を、Fast JSX: Don't clone props object #28768を参考にみていきました。個人的にはReactのjsxがどう動いているか調べるきっかけになりあとはずっとReactってcreateElement
を使ってると思っていたらこれがめちゃくちゃ古い知識だったと言うことに反省したりと学びが多かったです。
参考
0