Repository: facebook/react
Pull request: [Fizz] Responsive images should not be preloaded with link headers

Link headers are optionally supported for cases where you prefer to send resource loading hints before you're ready to send the body of a request. While many resources can be correctly preloaded from a link header responsive images are currently not supported and end up preloading the default src rather than the correctly sized image. Until responsive images are supported React will not allow these images to preload as headers and will retain them to preload as HTML.

closes: #32437 

Changed files:
filename: packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
patch: @@ -2958,6 +2958,9 @@ function pushImg(
       if (
         headers &&
         headers.remainingCapacity > 0 &&
+        // browsers today don't support preloading responsive images from link headers so we bail out
+        // if the img has srcset defined
+        typeof props.srcSet !== 'string' &&
         // this is a hueristic similar to capping element preloads to 10 unless explicitly
         // fetchPriority="high". We use length here which means it will fit fewer images when
         // the urls are long and more when short. arguably byte size is a better hueristic because
@@ -5703,6 +5706,10 @@ function preload(href: string, as: string, options?: ?PreloadImplOptions) {
         if (
           headers &&
           headers.remainingCapacity > 0 &&
+          // browsers today don't support preloading responsive images from link headers so we bail out
+          // if the img has srcset defined
+          typeof imageSrcSet !== 'string' &&
+          // We only include high priority images in the link header
           fetchPriority === 'high' &&
           // Compute the header since we might be able to fit it in the max length
           ((header = getPreloadAsHeader(href, as, options)),

filename: packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
patch: @@ -3723,23 +3723,43 @@ describe('ReactDOMFizzServer', () => {
     });
   });
 
-  it('encodes img srcset and sizes into preload header params', async () => {
+  it('omits images from preload headers if they contain srcset and sizes', async () => {
     let headers = null;
     function onHeaders(x) {
       headers = x;
     }
 
     function App() {
-      ReactDOM.preload('presrc', {
+      ReactDOM.preload('responsive-preload-set-only', {
+        as: 'image',
+        fetchPriority: 'high',
+        imageSrcSet: 'srcset',
+      });
+      ReactDOM.preload('responsive-preload', {
+        as: 'image',
+        fetchPriority: 'high',
+        imageSrcSet: 'srcset',
+        imageSizes: 'sizes',
+      });
+      ReactDOM.preload('non-responsive-preload', {
         as: 'image',
         fetchPriority: 'high',
-        imageSrcSet: 'presrcset',
-        imageSizes: 'presizes',
       });
       return (
         <html>
           <body>
-            <img src="src" srcSet="srcset" sizes="sizes" />
+            <img
+              src="responsive-img-set-only"
+              fetchPriority="high"
+              srcSet="srcset"
+            />
+            <img
+              src="responsive-img"
+              fetchPriority="high"
+              srcSet="srcset"
+              sizes="sizes"
+            />
+            <img src="non-responsive-img" fetchPriority="high" />
           </body>
         </html>
       );
@@ -3751,8 +3771,8 @@ describe('ReactDOMFizzServer', () => {
 
     expect(headers).toEqual({
       Link: `
-<presrc>; rel=preload; as="image"; fetchpriority="high"; imagesrcset="presrcset"; imagesizes="presizes",
- <src>; rel=preload; as="image"; imagesrcset="srcset"; imagesizes="sizes"
+<non-responsive-preload>; rel=preload; as="image"; fetchpriority="high", 
+<non-responsive-img>; rel=preload; as="image"; fetchpriority="high"
 `
         .replaceAll('\n', '')
         .trim(),