@samitouri / QOS-React / commits / d5e955d3c0

[compiler] Pass through unmodified props spread when inlining jsx (#30995)

If JSX receives a props spread without additional attributes (besides `ref` and `key`), we can pass the spread object as a property directly to avoid the extra object copy. ``` <Test {...propsToSpread} /> // {props: propsToSpread} <Test {...propsToSpread} a="z" /> // {props: {...propsToSpread, a: "z"}} ```

Jack Pope committed Sep 19, 2024 at 10:07 UTC d5e955d3c0c1fa2494de0ab33be9cd90c65aff1e
3 files changed +115 -56
compiler/packages/babel-plugin-react-compiler/src/Optimization/InlineJsxTransform.ts
+45 -18
@@ -23,6 +23,7 @@ import {
23 markPredecessors,
24 reversePostorderBlocks,
25 } from '../HIR/HIRBuilder';
26 +import {CompilerError} from '..';
27
28 function createSymbolProperty(
29 fn: HIRFunction,
@@ -151,6 +152,16 @@ function createPropsProperties(
152 let refProperty: ObjectProperty | undefined;
153 let keyProperty: ObjectProperty | undefined;
154 const props: Array<ObjectProperty | SpreadPattern> = [];
155 + const jsxAttributesWithoutKeyAndRef = propAttributes.filter(
156 + p => p.kind === 'JsxAttribute' && p.name !== 'key' && p.name !== 'ref',
157 + );
158 + const jsxSpreadAttributes = propAttributes.filter(
159 + p => p.kind === 'JsxSpreadAttribute',
160 + );
161 + const spreadPropsOnly =
162 + jsxAttributesWithoutKeyAndRef.length === 0 &&
163 + jsxSpreadAttributes.length === 1;
164 +
165 propAttributes.forEach(prop => {
166 switch (prop.kind) {
167 case 'JsxAttribute': {
@@ -180,7 +191,6 @@ function createPropsProperties(
191 break;
192 }
193 case 'JsxSpreadAttribute': {
183 - // TODO: Optimize spreads to pass object directly if none of its properties are mutated
194 props.push({
195 kind: 'Spread',
196 place: {...prop.argument},
@@ -189,6 +199,7 @@ function createPropsProperties(
199 }
200 }
201 });
202 +
203 const propsPropertyPlace = createTemporaryPlace(fn.env, instr.value.loc);
204 if (children) {
205 let childrenPropProperty: ObjectProperty;
@@ -268,23 +279,39 @@ function createPropsProperties(
279 nextInstructions.push(keyInstruction);
280 }
281
271 - const propsInstruction: Instruction = {
272 - id: makeInstructionId(0),
273 - lvalue: {...propsPropertyPlace, effect: Effect.Mutate},
274 - value: {
275 - kind: 'ObjectExpression',
276 - properties: props,
277 - loc: instr.value.loc,
278 - },
279 - loc: instr.loc,
280 - };
281 - const propsProperty: ObjectProperty = {
282 - kind: 'ObjectProperty',
283 - key: {name: 'props', kind: 'string'},
284 - type: 'property',
285 - place: {...propsPropertyPlace, effect: Effect.Capture},
286 - };
287 - nextInstructions.push(propsInstruction);
282 + let propsProperty: ObjectProperty;
283 + if (spreadPropsOnly) {
284 + const spreadProp = jsxSpreadAttributes[0];
285 + CompilerError.invariant(spreadProp.kind === 'JsxSpreadAttribute', {
286 + reason: 'Spread prop attribute must be of kind JSXSpreadAttribute',
287 + loc: instr.loc,
288 + });
289 + propsProperty = {
290 + kind: 'ObjectProperty',
291 + key: {name: 'props', kind: 'string'},
292 + type: 'property',
293 + place: {...spreadProp.argument, effect: Effect.Mutate},
294 + };
295 + } else {
296 + const propsInstruction: Instruction = {
297 + id: makeInstructionId(0),
298 + lvalue: {...propsPropertyPlace, effect: Effect.Mutate},
299 + value: {
300 + kind: 'ObjectExpression',
301 + properties: props,
302 + loc: instr.value.loc,
303 + },
304 + loc: instr.loc,
305 + };
306 + propsProperty = {
307 + kind: 'ObjectProperty',
308 + key: {name: 'props', kind: 'string'},
309 + type: 'property',
310 + place: {...propsPropertyPlace, effect: Effect.Capture},
311 + };
312 + nextInstructions.push(propsInstruction);
313 + }
314 +
315 return {refProperty, keyProperty, propsProperty};
316 }
317
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inline-jsx-transform.expect.md
+62 -35
@@ -28,9 +28,9 @@ function ParentAndRefAndKey(props) {
28 function ParentAndChildren(props) {
29 return (
30 <Parent>
31 - <Child key="a" />
31 + <Child key="a" {...props} />
32 <Child key="b">
33 - <GrandChild className={props.foo} />
33 + <GrandChild className={props.foo} {...props} />
34 </Child>
35 </Parent>
36 );
@@ -38,7 +38,12 @@ function ParentAndChildren(props) {
38
39 const propsToSpread = {a: 'a', b: 'b', c: 'c'};
40 function PropsSpread() {
41 - return <Test {...propsToSpread} />;
41 + return (
42 + <>
43 + <Test {...propsToSpread} />
44 + <Test {...propsToSpread} a="z" />
45 + </>
46 + );
47 }
48
49 export const FIXTURE_ENTRYPOINT = {
@@ -146,54 +151,59 @@ function ParentAndRefAndKey(props) {
151 }
152
153 function ParentAndChildren(props) {
149 - const $ = _c2(3);
154 + const $ = _c2(7);
155 let t0;
151 - if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
156 + if ($[0] !== props) {
157 t0 = {
158 $$typeof: Symbol.for("react.transitional.element"),
159 type: Child,
160 ref: null,
161 key: "a",
157 - props: {},
162 + props: props,
163 };
159 - $[0] = t0;
164 + $[0] = props;
165 + $[1] = t0;
166 } else {
161 - t0 = $[0];
167 + t0 = $[1];
168 }
169 let t1;
164 - if ($[1] !== props.foo) {
170 + if ($[2] !== props) {
171 t1 = {
172 $$typeof: Symbol.for("react.transitional.element"),
167 - type: Parent,
173 + type: Child,
174 ref: null,
169 - key: null,
175 + key: "b",
176 props: {
171 - children: [
172 - t0,
173 - {
174 - $$typeof: Symbol.for("react.transitional.element"),
175 - type: Child,
176 - ref: null,
177 - key: "b",
178 - props: {
179 - children: {
180 - $$typeof: Symbol.for("react.transitional.element"),
181 - type: GrandChild,
182 - ref: null,
183 - key: null,
184 - props: { className: props.foo },
185 - },
186 - },
187 - },
188 - ],
177 + children: {
178 + $$typeof: Symbol.for("react.transitional.element"),
179 + type: GrandChild,
180 + ref: null,
181 + key: null,
182 + props: { className: props.foo, ...props },
183 + },
184 },
185 };
191 - $[1] = props.foo;
192 - $[2] = t1;
186 + $[2] = props;
187 + $[3] = t1;
188 } else {
194 - t1 = $[2];
189 + t1 = $[3];
190 }
196 - return t1;
191 + let t2;
192 + if ($[4] !== t0 || $[5] !== t1) {
193 + t2 = {
194 + $$typeof: Symbol.for("react.transitional.element"),
195 + type: Parent,
196 + ref: null,
197 + key: null,
198 + props: { children: [t0, t1] },
199 + };
200 + $[4] = t0;
201 + $[5] = t1;
202 + $[6] = t2;
203 + } else {
204 + t2 = $[6];
205 + }
206 + return t2;
207 }
208
209 const propsToSpread = { a: "a", b: "b", c: "c" };
@@ -203,10 +213,27 @@ function PropsSpread() {
213 if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
214 t0 = {
215 $$typeof: Symbol.for("react.transitional.element"),
206 - type: Test,
216 + type: Symbol.for("react.fragment"),
217 ref: null,
218 key: null,
209 - props: { ...propsToSpread },
219 + props: {
220 + children: [
221 + {
222 + $$typeof: Symbol.for("react.transitional.element"),
223 + type: Test,
224 + ref: null,
225 + key: null,
226 + props: propsToSpread,
227 + },
228 + {
229 + $$typeof: Symbol.for("react.transitional.element"),
230 + type: Test,
231 + ref: null,
232 + key: null,
233 + props: { ...propsToSpread, a: "z" },
234 + },
235 + ],
236 + },
237 };
238 $[0] = t0;
239 } else {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inline-jsx-transform.js
+8 -3
@@ -24,9 +24,9 @@ function ParentAndRefAndKey(props) {
24 function ParentAndChildren(props) {
25 return (
26 <Parent>
27 - <Child key="a" />
27 + <Child key="a" {...props} />
28 <Child key="b">
29 - <GrandChild className={props.foo} />
29 + <GrandChild className={props.foo} {...props} />
30 </Child>
31 </Parent>
32 );
@@ -34,7 +34,12 @@ function ParentAndChildren(props) {
34
35 const propsToSpread = {a: 'a', b: 'b', c: 'c'};
36 function PropsSpread() {
37 - return <Test {...propsToSpread} />;
37 + return (
38 + <>
39 + <Test {...propsToSpread} />
40 + <Test {...propsToSpread} a="z" />
41 + </>
42 + );
43 }
44
45 export const FIXTURE_ENTRYPOINT = {