Skip to content

Commit 2c3ae5e

Browse files
gnoffAndyPengc12
authored andcommitted
[Fizz][Float] <img> inside <picture> should not preload during SSR (facebook#27346)
img tags inside picture tags should not automatically be preloaded because usually the img is a fallback. We will consider a more comprehensive way of preloading picture tags which may require a technique like using an inline script to construct the image in the browser but for now we simply omit the preloads to avoid harming load times by loading fallbacks.
1 parent a364e60 commit 2c3ae5e

File tree

2 files changed

+75
-40
lines changed

2 files changed

+75
-40
lines changed

packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js

Lines changed: 41 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -499,22 +499,26 @@ const HTML_COLGROUP_MODE = 8;
499499

500500
type InsertionMode = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
501501

502+
const NO_SCOPE = /* */ 0b00;
503+
const NOSCRIPT_SCOPE = /* */ 0b01;
504+
const PICTURE_SCOPE = /* */ 0b10;
505+
502506
// Lets us keep track of contextual state and pick it back up after suspending.
503507
export type FormatContext = {
504508
insertionMode: InsertionMode, // root/svg/html/mathml/table
505509
selectedValue: null | string | Array<string>, // the selected value(s) inside a <select>, or null outside <select>
506-
noscriptTagInScope: boolean,
510+
tagScope: number,
507511
};
508512

509513
function createFormatContext(
510514
insertionMode: InsertionMode,
511515
selectedValue: null | string,
512-
noscriptTagInScope: boolean,
516+
tagScope: number,
513517
): FormatContext {
514518
return {
515519
insertionMode,
516520
selectedValue,
517-
noscriptTagInScope,
521+
tagScope,
518522
};
519523
}
520524

@@ -525,7 +529,7 @@ export function createRootFormatContext(namespaceURI?: string): FormatContext {
525529
: namespaceURI === 'http://www.w3.org/1998/Math/MathML'
526530
? MATHML_MODE
527531
: ROOT_HTML_MODE;
528-
return createFormatContext(insertionMode, null, false);
532+
return createFormatContext(insertionMode, null, NO_SCOPE);
529533
}
530534

531535
export function getChildFormatContext(
@@ -535,80 +539,70 @@ export function getChildFormatContext(
535539
): FormatContext {
536540
switch (type) {
537541
case 'noscript':
538-
return createFormatContext(HTML_MODE, null, true);
542+
return createFormatContext(
543+
HTML_MODE,
544+
null,
545+
parentContext.tagScope | NOSCRIPT_SCOPE,
546+
);
539547
case 'select':
540548
return createFormatContext(
541549
HTML_MODE,
542550
props.value != null ? props.value : props.defaultValue,
543-
parentContext.noscriptTagInScope,
551+
parentContext.tagScope,
544552
);
545553
case 'svg':
554+
return createFormatContext(SVG_MODE, null, parentContext.tagScope);
555+
case 'picture':
546556
return createFormatContext(
547-
SVG_MODE,
557+
HTML_MODE,
548558
null,
549-
parentContext.noscriptTagInScope,
559+
parentContext.tagScope | PICTURE_SCOPE,
550560
);
551561
case 'math':
552-
return createFormatContext(
553-
MATHML_MODE,
554-
null,
555-
parentContext.noscriptTagInScope,
556-
);
562+
return createFormatContext(MATHML_MODE, null, parentContext.tagScope);
557563
case 'foreignObject':
558-
return createFormatContext(
559-
HTML_MODE,
560-
null,
561-
parentContext.noscriptTagInScope,
562-
);
564+
return createFormatContext(HTML_MODE, null, parentContext.tagScope);
563565
// Table parents are special in that their children can only be created at all if they're
564566
// wrapped in a table parent. So we need to encode that we're entering this mode.
565567
case 'table':
566-
return createFormatContext(
567-
HTML_TABLE_MODE,
568-
null,
569-
parentContext.noscriptTagInScope,
570-
);
568+
return createFormatContext(HTML_TABLE_MODE, null, parentContext.tagScope);
571569
case 'thead':
572570
case 'tbody':
573571
case 'tfoot':
574572
return createFormatContext(
575573
HTML_TABLE_BODY_MODE,
576574
null,
577-
parentContext.noscriptTagInScope,
575+
parentContext.tagScope,
578576
);
579577
case 'colgroup':
580578
return createFormatContext(
581579
HTML_COLGROUP_MODE,
582580
null,
583-
parentContext.noscriptTagInScope,
581+
parentContext.tagScope,
584582
);
585583
case 'tr':
586584
return createFormatContext(
587585
HTML_TABLE_ROW_MODE,
588586
null,
589-
parentContext.noscriptTagInScope,
587+
parentContext.tagScope,
590588
);
591589
}
592590
if (parentContext.insertionMode >= HTML_TABLE_MODE) {
593591
// Whatever tag this was, it wasn't a table parent or other special parent, so we must have
594592
// entered plain HTML again.
595-
return createFormatContext(
596-
HTML_MODE,
597-
null,
598-
parentContext.noscriptTagInScope,
599-
);
593+
return createFormatContext(HTML_MODE, null, parentContext.tagScope);
600594
}
601595
if (parentContext.insertionMode === ROOT_HTML_MODE) {
602596
if (type === 'html') {
603597
// We've emitted the root and is now in <html> mode.
604-
return createFormatContext(HTML_HTML_MODE, null, false);
598+
return createFormatContext(HTML_HTML_MODE, null, parentContext.tagScope);
605599
} else {
606600
// We've emitted the root and is now in plain HTML mode.
607-
return createFormatContext(HTML_MODE, null, false);
601+
return createFormatContext(HTML_MODE, null, parentContext.tagScope);
608602
}
609603
} else if (parentContext.insertionMode === HTML_HTML_MODE) {
610604
// We've emitted the document element and is now in plain HTML mode.
611-
return createFormatContext(HTML_MODE, null, false);
605+
return createFormatContext(HTML_MODE, null, parentContext.tagScope);
612606
}
613607
return parentContext;
614608
}
@@ -2457,12 +2451,14 @@ function pushImg(
24572451
target: Array<Chunk | PrecomputedChunk>,
24582452
props: Object,
24592453
resumableState: ResumableState,
2454+
pictureTagInScope: boolean,
24602455
): null {
24612456
const {src, srcSet} = props;
24622457
if (
24632458
props.loading !== 'lazy' &&
24642459
(typeof src === 'string' || typeof srcSet === 'string') &&
24652460
props.fetchPriority !== 'low' &&
2461+
pictureTagInScope === false &&
24662462
// We exclude data URIs in src and srcSet since these should not be preloaded
24672463
!(
24682464
typeof src === 'string' &&
@@ -3230,7 +3226,7 @@ export function pushStartInstance(
32303226
props,
32313227
renderState,
32323228
formatContext.insertionMode,
3233-
formatContext.noscriptTagInScope,
3229+
!!(formatContext.tagScope & NOSCRIPT_SCOPE),
32343230
)
32353231
: pushStartTitle(target, props);
32363232
case 'link':
@@ -3241,7 +3237,7 @@ export function pushStartInstance(
32413237
renderState,
32423238
textEmbedded,
32433239
formatContext.insertionMode,
3244-
formatContext.noscriptTagInScope,
3240+
!!(formatContext.tagScope & NOSCRIPT_SCOPE),
32453241
);
32463242
case 'script':
32473243
return enableFloat
@@ -3251,7 +3247,7 @@ export function pushStartInstance(
32513247
resumableState,
32523248
textEmbedded,
32533249
formatContext.insertionMode,
3254-
formatContext.noscriptTagInScope,
3250+
!!(formatContext.tagScope & NOSCRIPT_SCOPE),
32553251
)
32563252
: pushStartGenericElement(target, props, type);
32573253
case 'style':
@@ -3262,7 +3258,7 @@ export function pushStartInstance(
32623258
renderState,
32633259
textEmbedded,
32643260
formatContext.insertionMode,
3265-
formatContext.noscriptTagInScope,
3261+
!!(formatContext.tagScope & NOSCRIPT_SCOPE),
32663262
);
32673263
case 'meta':
32683264
return pushMeta(
@@ -3271,7 +3267,7 @@ export function pushStartInstance(
32713267
renderState,
32723268
textEmbedded,
32733269
formatContext.insertionMode,
3274-
formatContext.noscriptTagInScope,
3270+
!!(formatContext.tagScope & NOSCRIPT_SCOPE),
32753271
);
32763272
// Newline eating tags
32773273
case 'listing':
@@ -3280,7 +3276,12 @@ export function pushStartInstance(
32803276
}
32813277
case 'img': {
32823278
return enableFloat
3283-
? pushImg(target, props, resumableState)
3279+
? pushImg(
3280+
target,
3281+
props,
3282+
resumableState,
3283+
!!(formatContext.tagScope & PICTURE_SCOPE),
3284+
)
32843285
: pushSelfClosing(target, props, type);
32853286
}
32863287
// Omitted close tags

packages/react-dom/src/__tests__/ReactDOMFloat-test.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4085,6 +4085,40 @@ body {
40854085
);
40864086
});
40874087

4088+
// https://github.com/vercel/next.js/discussions/54799
4089+
it('omits preloads when an <img> is inside a <picture>', async () => {
4090+
await act(() => {
4091+
renderToPipeableStream(
4092+
<html>
4093+
<body>
4094+
<picture>
4095+
<img src="foo" />
4096+
</picture>
4097+
<picture>
4098+
<source type="image/webp" srcSet="webpsrc" />
4099+
<img src="jpg fallback" />
4100+
</picture>
4101+
</body>
4102+
</html>,
4103+
).pipe(writable);
4104+
});
4105+
4106+
expect(getMeaningfulChildren(document)).toEqual(
4107+
<html>
4108+
<head />
4109+
<body>
4110+
<picture>
4111+
<img src="foo" />
4112+
</picture>
4113+
<picture>
4114+
<source type="image/webp" srcset="webpsrc" />
4115+
<img src="jpg fallback" />
4116+
</picture>
4117+
</body>
4118+
</html>,
4119+
);
4120+
});
4121+
40884122
describe('ReactDOM.prefetchDNS(href)', () => {
40894123
it('creates a dns-prefetch resource when called', async () => {
40904124
function App({url}) {

0 commit comments

Comments
 (0)