[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 = {