🌲

enableForestを使うことでReact Compilerのメモ化レベルを変更できる

2024/06/02に公開

Intro

先日Youtubeで公開したReact Compiler Code reading #1の中で出てきた謎オプション、enableForestについてまとめました。

https://github.com/facebook/react/blob/d77dd31a329df55a051800fc76668af8da8332b4/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts#L175-L176

コメントが🌲です。さすがに二度見します。

6/5 追記

Xにて、React teamのJoe Savonaさんにコメントいただきました!
オプションをいじればバグが起きてしまう可能性があるので、できればデフォルトのままの方が良いとのことです!

https://x.com/en_JS/status/1797305596498317744

結論

React Compilerのメモ化のレベルを変更できるオプションです。名前とコメントのせいで怪しく見えますが、意外とちゃんと使われてます。

code

以下のようなサンプルコードを用意しました。シンプルなReactのコードになっていると思います。

function Title() {
  return (
    <div>
      <h1>My React App</h1>
      <p>Welcome to my simple React application!</p>
    </div>
  );
}

function Counter() {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount(count + 1);
  };

  const decrement = () => {
    setCount(count - 1);
  };

  return (
    <div>
      <h2>Counter App</h2>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
    </div>
  );
}

function App() {
  return (
    <div>
      <Title />
      <Counter />
    </div>
  );
}

以下のようにそれぞれコードが出力されます

enableForest off
function Title() {
  const $ = _c(1);

  let t0;

  if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
    t0 = (
      <div>
        <h1>My React App</h1>
        <p>Welcome to my simple React application!</p>
      </div>
    );
    $[0] = t0;
  } else {
    t0 = $[0];
  }

  return t0;
}

function Counter() {
  const $ = _c(15);

  const [count, setCount] = useState(0);
  let t0;

  if ($[0] !== count) {
    t0 = () => {
      setCount(count + 1);
    };

    $[0] = count;
    $[1] = t0;
  } else {
    t0 = $[1];
  }

  const increment = t0;
  let t1;

  if ($[2] !== count) {
    t1 = () => {
      setCount(count - 1);
    };

    $[2] = count;
    $[3] = t1;
  } else {
    t1 = $[3];
  }

  const decrement = t1;
  let t2;

  if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
    t2 = <h2>Counter App</h2>;
    $[4] = t2;
  } else {
    t2 = $[4];
  }

  let t3;

  if ($[5] !== count) {
    t3 = <p>Count: {count}</p>;
    $[5] = count;
    $[6] = t3;
  } else {
    t3 = $[6];
  }

  let t4;

  if ($[7] !== increment) {
    t4 = <button onClick={increment}>Increment</button>;
    $[7] = increment;
    $[8] = t4;
  } else {
    t4 = $[8];
  }

  let t5;

  if ($[9] !== decrement) {
    t5 = <button onClick={decrement}>Decrement</button>;
    $[9] = decrement;
    $[10] = t5;
  } else {
    t5 = $[10];
  }

  let t6;

  if ($[11] !== t3 || $[12] !== t4 || $[13] !== t5) {
    t6 = (
      <div>
        {t2}
        {t3}
        {t4}
        {t5}
      </div>
    );
    $[11] = t3;
    $[12] = t4;
    $[13] = t5;
    $[14] = t6;
  } else {
    t6 = $[14];
  }

  return t6;
}

function App() {
  const $ = _c(2);

  let t0;

  if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
    t0 = <Title />;
    $[0] = t0;
  } else {
    t0 = $[0];
  }

  let t1;

  if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
    t1 = (
      <div>
        {t0}
        <Counter />
      </div>
    );
    $[1] = t1;
  } else {
    t1 = $[1];
  }

  return t1;
}
enableForest on
function Title() {
  return (
    <div>
      <h1>My React App</h1>
      <p>Welcome to my simple React application!</p>
    </div>
  );
}

function Counter() {
  const $ = _c(4);

  const [count, setCount] = useState(0);
  let t0;

  if ($[0] !== count) {
    t0 = () => {
      setCount(count + 1);
    };

    $[0] = count;
    $[1] = t0;
  } else {
    t0 = $[1];
  }

  const increment = t0;
  let t1;

  if ($[2] !== count) {
    t1 = () => {
      setCount(count - 1);
    };

    $[2] = count;
    $[3] = t1;
  } else {
    t1 = $[3];
  }

  const decrement = t1;
  return (
    <div>
      <h2>Counter App</h2>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
    </div>
  );
}

function App() {
  return (
    <div>
      <Title />
      <Counter />
    </div>
  );
}

違い

// enableForestをon
function Title() {
  return (
    <div>
      <h1>My React App</h1>
      <p>Welcome to my simple React application!</p>
    </div>
  );
}

// enableForestをoff
function Title() {
  const $ = _c(1);

  let t0;

  if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
    t0 = (
      <div>
        <h1>My React App</h1>
        <p>Welcome to my simple React application!</p>
      </div>
    );
    $[0] = t0;
  } else {
    t0 = $[0];
  }

  return t0;
}

enableForestを有効にしたことでまずわかるのは、Titleコンポーネントがほぼ何も変わってないことです。
このオプションはメモ化のレベルを変更すると最初に言いましたが、jsxに関してはほぼメモ化しないようになるみたいです。

実装

https://github.com/facebook/react/blob/d77dd31a329df55a051800fc76668af8da8332b4/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonEscapingScopes.ts#L808-L815

↑ enableForestの値によって、memoizeJsxElementsforceMemoizePrimitivesの値が変わっています。

https://github.com/facebook/react/blob/d77dd31a329df55a051800fc76668af8da8332b4/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonEscapingScopes.ts#L430-L432

↑ memoizeJsxElementsの値によって、メモ化のレベルを変えています。
このmemoizeJsxElementsは二箇所で使用されています。

  • JsxExpression: IdentifierはComponent、JsxAttributeがHTMLタグ
  • JsxFragment: フラグメント

↑ に対してのメモ化がなくなります。なのでほぼjsxに対してのメモ化はなくなるということですね。

あとforceMemoizePrimitivesというものもあります。これはenableForestがtrueだったらメモ化を行います。
https://github.com/facebook/react/blob/d77dd31a329df55a051800fc76668af8da8332b4/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonEscapingScopes.ts#L466-L469

これは三箇所で使用されています。

  • UnaryExpression: "void" | "throw" | "delete" | "!" | "+" | "-" | "~" | "typeof"
  • PropertyLoad: propsにオブジェクトプロパティのロード
  • ComputedLoad: props["exmaple"]みたいにアクセスしたときのロード

なんとなくそこそこ使えそうなオプションだなということが理解できますね。個人的には全てのメモ化を下げず、下げたい時に"use no memo"とか使えばいいんじゃないかなと思ったりしますが👀

まとめ

  • enableForestオプションとは、React Compilerのメモ化レベルを変更できるオプション
  • memoizeJsxElementsはenableForestがtrueだったらjsxをメモ化しないようにする
  • forceMemoizePrimitivesはenableForestがtrueだったら演算子やpropsのオブジェクトに対してメモ化を行う

引き続きReact Compilerのコードを見てブログ書いていきます。

関連

thanks🙏

Discussion