@samitouri / QOS-React-2 / commits / 3566de59e2

[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}) {