react icon indicating copy to clipboard operation
react copied to clipboard

JSX Outlining

Open gsathya opened this issue 5 months ago • 5 comments

Currently, the react compiler can not compile within callbacks which can potentially cause over rendering. Consider this example:

function Component(countries, onDelete) {
  const name = useFoo();
  return countries.map(() => {
    return (
      <Foo>
        <Bar name={name}/>
        <Baz onclick={onDelete} />
      </Foo>
    );
  });
}

In this case, there's no memoization of the nested jsx elements. But instead if we were to manually refactor the nested jsx into separate component like this:

function Component(countries, onDelete) {
  const name = useFoo();
  return countries.map(() => {
    return <Temp name={name} onDelete={onDelete} />;
  });
}

function Temp({ name, onDelete }) {
  return (
    <Foo>
      <Bar name={name} />
      <Baz onclick={onDelete} />
    </Foo>
  );
}

The compiler can now optimise both these components:

function Component(countries, onDelete) {
  const $ = _c(4);
  const name = useFoo();
  let t0;
  if ($[0] !== name || $[1] !== onDelete || $[2] !== countries) {
    t0 = countries.map(() => <Temp name={name} onDelete={onDelete} />);
    $[0] = name;
    $[1] = onDelete;
    $[2] = countries;
    $[3] = t0;
  } else {
    t0 = $[3];
  }
  return t0;
}

function Temp(t0) {
  const $ = _c(7);
  const { name, onDelete } = t0;
  let t1;
  if ($[0] !== name) {
    t1 = <Bar name={name} />;
    $[0] = name;
    $[1] = t1;
  } else {
    t1 = $[1];
  }
  let t2;
  if ($[2] !== onDelete) {
    t2 = <Baz onclick={onDelete} />;
    $[2] = onDelete;
    $[3] = t2;
  } else {
    t2 = $[3];
  }
  let t3;
  if ($[4] !== t1 || $[5] !== t2) {
    t3 = (
      <Foo>
        {t1}
        {t2}
      </Foo>
    );
    $[4] = t1;
    $[5] = t2;
    $[6] = t3;
  } else {
    t3 = $[6];
  }
  return t3;
}

Now, when countries is updated by adding one single value, only the newly added value is re-rendered and not the entire list. Rather than having to do this manually, this PR teaches the react compiler to do this transformation.

This PR adds a new pass (OutlineJsx) to capture nested jsx statements and outline them in a separate component. This newly outlined component can then by memoized by the compiler, giving us more fine grained rendering.

This PR adds a new HIR node (StartJsx) to track when the jsx statement starts, which lets us start capturing nested jsx expressions.

There's a lot of improvements we can do in the future:

  • For now, we only outline nested jsx inside callbacks, but this can be extended in the future (for ex, to outline nested jsx inside loops).
function Component(arr) {
  const jsx = [];
  for (const i of arr) {
    jsx.push(
      // this nested jsx can be outlined
      <Bar>
        <Baz i={i}></Baz>
      </Bar>,
    );
  }
  return jsx;
}
  • Only the JSX expression statements are outlined, none of the other statements that flow into the jsx are outlined. This is a bit tricky as we must only outline non-mutating statements using our effects analysis.
function Component(arr) {
  return arr.map((i) => {
    // this statement should not be outlined
    const y = mutate(i);
    // this statement can be outlined
    const x = { i };
    return (
      <Bar>
        <Baz x={x} y={y}></Baz>
      </Bar>
    );
  });
}

gsathya avatar Sep 13 '24 16:09 gsathya