[Fizz][Float] `<img>` inside `<picture>` should not preload during SSR (#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.
Josh Story committed
Sep 7, 2023 at 12:48 UTC
3566de59e2046e7e8478462375aaa71716f1095b
2 files changed
+75
-40
packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
+41
-40
@@ -499,22 +499,26 @@ const HTML_COLGROUP_MODE = 8;
499
500
type InsertionMode = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
501
502
+const NO_SCOPE = /* */ 0b00;
503
+const NOSCRIPT_SCOPE = /* */ 0b01;
504
+const PICTURE_SCOPE = /* */ 0b10;
505
+
506
// Lets us keep track of contextual state and pick it back up after suspending.
507
export type FormatContext = {
508
insertionMode: InsertionMode, // root/svg/html/mathml/table
509
selectedValue: null | string | Array<string>, // the selected value(s) inside a <select>, or null outside <select>
506
- noscriptTagInScope: boolean,
510
+ tagScope: number,
511
};
512
513
function createFormatContext(
514
insertionMode: InsertionMode,
515
selectedValue: null | string,
512
- noscriptTagInScope: boolean,
516
+ tagScope: number,
517
): FormatContext {
518
return {
519
insertionMode,
520
selectedValue,
517
- noscriptTagInScope,
521
+ tagScope,
522
};
523
}
524
@@ -525,7 +529,7 @@ export function createRootFormatContext(namespaceURI?: string): FormatContext {
529
: namespaceURI === 'http://www.w3.org/1998/Math/MathML'
530
? MATHML_MODE
531
: ROOT_HTML_MODE;
528
- return createFormatContext(insertionMode, null, false);
532
+ return createFormatContext(insertionMode, null, NO_SCOPE);
533
}
534
535
export function getChildFormatContext(
@@ -535,80 +539,70 @@ export function getChildFormatContext(
539
): FormatContext {
540
switch (type) {
541
case 'noscript':
538
- return createFormatContext(HTML_MODE, null, true);
542
+ return createFormatContext(
543
+ HTML_MODE,
544
+ null,
545
+ parentContext.tagScope | NOSCRIPT_SCOPE,
546
+ );
547
case 'select':
548
return createFormatContext(
549
HTML_MODE,
550
props.value != null ? props.value : props.defaultValue,
543
- parentContext.noscriptTagInScope,
551
+ parentContext.tagScope,
552
);
553
case 'svg':
554
+ return createFormatContext(SVG_MODE, null, parentContext.tagScope);
555
+ case 'picture':
556
return createFormatContext(
547
- SVG_MODE,
557
+ HTML_MODE,
558
null,
549
- parentContext.noscriptTagInScope,
559
+ parentContext.tagScope | PICTURE_SCOPE,
560
);
561
case 'math':
552
- return createFormatContext(
553
- MATHML_MODE,
554
- null,
555
- parentContext.noscriptTagInScope,
556
- );
562
+ return createFormatContext(MATHML_MODE, null, parentContext.tagScope);
563
case 'foreignObject':
558
- return createFormatContext(
559
- HTML_MODE,
560
- null,
561
- parentContext.noscriptTagInScope,
562
- );
564
+ return createFormatContext(HTML_MODE, null, parentContext.tagScope);
565
// Table parents are special in that their children can only be created at all if they're
566
// wrapped in a table parent. So we need to encode that we're entering this mode.
567
case 'table':
566
- return createFormatContext(
567
- HTML_TABLE_MODE,
568
- null,
569
- parentContext.noscriptTagInScope,
570
- );
568
+ return createFormatContext(HTML_TABLE_MODE, null, parentContext.tagScope);
569
case 'thead':
570
case 'tbody':
571
case 'tfoot':
572
return createFormatContext(
573
HTML_TABLE_BODY_MODE,
574
null,
577
- parentContext.noscriptTagInScope,
575
+ parentContext.tagScope,
576
);
577
case 'colgroup':
578
return createFormatContext(
579
HTML_COLGROUP_MODE,
580
null,
583
- parentContext.noscriptTagInScope,
581
+ parentContext.tagScope,
582
);
583
case 'tr':
584
return createFormatContext(
585
HTML_TABLE_ROW_MODE,
586
null,
589
- parentContext.noscriptTagInScope,
587
+ parentContext.tagScope,
588
);
589
}
590
if (parentContext.insertionMode >= HTML_TABLE_MODE) {
591
// Whatever tag this was, it wasn't a table parent or other special parent, so we must have
592
// entered plain HTML again.
595
- return createFormatContext(
596
- HTML_MODE,
597
- null,
598
- parentContext.noscriptTagInScope,
599
- );
593
+ return createFormatContext(HTML_MODE, null, parentContext.tagScope);
594
}
595
if (parentContext.insertionMode === ROOT_HTML_MODE) {
596
if (type === 'html') {
597
// 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);
599
} else {
600
// 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);
602
}
603
} else if (parentContext.insertionMode === HTML_HTML_MODE) {
604
// 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);
606
}
607
return parentContext;
608
}
@@ -2457,12 +2451,14 @@ function pushImg(
2451
target: Array<Chunk | PrecomputedChunk>,
2452
props: Object,
2453
resumableState: ResumableState,
2454
+ pictureTagInScope: boolean,
2455
): null {
2456
const {src, srcSet} = props;
2457
if (
2458
props.loading !== 'lazy' &&
2459
(typeof src === 'string' || typeof srcSet === 'string') &&
2460
props.fetchPriority !== 'low' &&
2461
+ pictureTagInScope === false &&
2462
// We exclude data URIs in src and srcSet since these should not be preloaded
2463
!(
2464
typeof src === 'string' &&
@@ -3230,7 +3226,7 @@ export function pushStartInstance(
3226
props,
3227
renderState,
3228
formatContext.insertionMode,
3233
- formatContext.noscriptTagInScope,
3229
+ !!(formatContext.tagScope & NOSCRIPT_SCOPE),
3230
)
3231
: pushStartTitle(target, props);
3232
case 'link':
@@ -3241,7 +3237,7 @@ export function pushStartInstance(
3237
renderState,
3238
textEmbedded,
3239
formatContext.insertionMode,
3244
- formatContext.noscriptTagInScope,
3240
+ !!(formatContext.tagScope & NOSCRIPT_SCOPE),
3241
);
3242
case 'script':
3243
return enableFloat
@@ -3251,7 +3247,7 @@ export function pushStartInstance(
3247
resumableState,
3248
textEmbedded,
3249
formatContext.insertionMode,
3254
- formatContext.noscriptTagInScope,
3250
+ !!(formatContext.tagScope & NOSCRIPT_SCOPE),
3251
)
3252
: pushStartGenericElement(target, props, type);
3253
case 'style':
@@ -3262,7 +3258,7 @@ export function pushStartInstance(
3258
renderState,
3259
textEmbedded,
3260
formatContext.insertionMode,
3265
- formatContext.noscriptTagInScope,
3261
+ !!(formatContext.tagScope & NOSCRIPT_SCOPE),
3262
);
3263
case 'meta':
3264
return pushMeta(
@@ -3271,7 +3267,7 @@ export function pushStartInstance(
3267
renderState,
3268
textEmbedded,
3269
formatContext.insertionMode,
3274
- formatContext.noscriptTagInScope,
3270
+ !!(formatContext.tagScope & NOSCRIPT_SCOPE),
3271
);
3272
// Newline eating tags
3273
case 'listing':
@@ -3280,7 +3276,12 @@ export function pushStartInstance(
3276
}
3277
case 'img': {
3278
return enableFloat
3283
- ? pushImg(target, props, resumableState)
3279
+ ? pushImg(
3280
+ target,
3281
+ props,
3282
+ resumableState,
3283
+ !!(formatContext.tagScope & PICTURE_SCOPE),
3284
+ )
3285
: pushSelfClosing(target, props, type);
3286
}
3287
// Omitted close tags
packages/react-dom/src/__tests__/ReactDOMFloat-test.js
+34
@@ -4085,6 +4085,40 @@ body {
4085
);
4086
});
4087
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
+
4122
describe('ReactDOM.prefetchDNS(href)', () => {
4123
it('creates a dns-prefetch resource when called', async () => {
4124
function App({url}) {