Skip to content

Bug: Random preloads added for images #27910

Closed

Description

@muteor

React version: 18.3.0-canary-c29ca23af-20231205

Steps To Reproduce

  1. Render SSR app that has images in its tree using renderToString or renderToPipeableStream

Link to code example:

The current behavior

image

The expected behavior

To not insert preloads randomly.

Note that there is difference between renderToString and renderToPipeableStream, renderToString the preloads appear inside root and renderToPipeableStream they appear in head.

Sorry this is a bit of a question rather than bug report maybe, I have searched around and cannot see any mention of auto-preloading images but I assume there must be something going on that I have missed.

I was able to fix by using 18.3.0-canary-493f72b0a-20230727.

Activity

  1. nickluger commented on Mar 30, 2024

    @nickluger

    This. Tested with 19.0.0-canary-a73c3450e-20240329.

    It would be "kinda" tolerable for the LCP image above fold (featured image or similar), but we have lots of lazy loaded images below fold which contain alternatives like:

    <img
      {...imgProps}
    />
    <noscript>
      <img
        alt={alt}
        src={noscriptSrc}
      />
    </noscript>

    For every <img> tag used in noscript, the image is preloaded in <head> which completely defies the purpose of lazy loading.

  2. muteor commented on Apr 5, 2024

    @muteor
    Author

    I think this is actually now expected behaviour, after digging the source a bit I found f359f9b

    Though I could not find any docs/release notes that mention this which is a little odd as now you need to explicitly think about the loading strategy of every img tag you use. For me I was able to "fix" this by making sure all our img tags have either loading=lazy or fetchPriority=low when they are not to be preloaded, this gave me more or less only the LCP images preloaded.

    @nickluger For your case you could either add loading=lazy or fetchPriority=low to the fallback images, though depending on your target browsers you could get rid of the noscript parts these days as loading=lazy has pretty wide browser support now.

    Would be good to have this documented somewhere and also maybe add this to the react linter.

  3. nickluger commented on Apr 5, 2024

    @nickluger

    Thanks for the info regarding loading=lazy, that could solve it. 🙏

    If this is really a "feature", I have to say, I don't think it should be React's business to put stuff into my <head> automagically…

  4. added and removed
    Status: UnconfirmedA potential issue that we haven't yet confirmed as a bug
    on Apr 5, 2024
  5. eps1lon commented on Apr 10, 2024

    @eps1lon
    Collaborator

    @muteor Which framework are you using?

  6. nickluger commented on Apr 10, 2024

    @nickluger

    For me, its Remix.

  7. eps1lon commented on Apr 10, 2024

    @eps1lon
    Collaborator

    You're probably using their image component that uses ReactDOM.preload. Or they're doing ReactDOM.preload calls some other way. Can you open an issue on their issue tracker first to clarify?

  8. nickluger commented on Apr 10, 2024

    @nickluger

    I'm not using their image component but <img>, but I will take a look at Remix & React Router code, whether they call this somewhere and open an issue. Interestingly, this behavior is only applied by Remix (or happens for some reason), when upgrading to React canary.

  9. gnoff commented on Apr 10, 2024

    @gnoff
    Collaborator

    React is adding support for Suspensey "resource". For instance if you render a stylesheet and opt into React's handling of this using "precedence" then commits which contain this stylesheet will wait to commit until the sheet has loaded.

    Another area that we're making Suspensey is images. Lazy images indicate that they do not require suspense coordination but by default images will (in the future) block a boundary reveal until the image has loaded and been decoded.

    While these features are for client updates there are analogs for SSR. for stylesheets browsers already block paint until stylesheets load in the head. They don't however do this for images. React canaries now preload the first few images rendered if they are not lazy because we want to mimic the coordinated reveal of Suspensey images along with the first paint. It's not a perfect heuristic because it's not actually enforced to be coordinated but it does better align the behavior across suspensy vs lazy images.

    Another way you can signal to React that an image is not expected to be part of the first paint is to set the fetchPriority to "low"

  10. gnoff commented on Apr 10, 2024

    @gnoff
    Collaborator

    For every tag used in noscript, the image is preloaded in which completely defies the purpose of lazy loading.

    This is a genuine bug. will land a fix

  11. nickluger commented on Apr 10, 2024

    @nickluger

    Good to know, thanks for the explanation. 👍

  12. muteor commented on Apr 10, 2024

    @muteor
    Author

    fwiw no framework used just renderToPipeableStream, in my case I was able to add low priority to a few icon images to get the preload to load reasonable candidates.

    Would be good to be able to opt-out of this somehow for a boundary, thinking of the case where you are waiting on component/data to load and don't really care about the image but lazy loading would be inappropriate for an above the fold larger image. Maybe even a way to modify the heuristic via some sort of callback would be useful.

  13. added a commit that references this issue on Apr 10, 2024
    dc6a7e0
  14. added a commit that references this issue on Apr 10, 2024
    05f6de5
  15. added 2 commits that reference this issue on Apr 11, 2024
    deccced
    7c86590
  16. added a commit that references this issue on Apr 15, 2024
    5919739
  17. imjordanxd commented on Mar 13, 2025

    @imjordanxd

    I recently ran into this issue as well for snapshot tests. It'd be good if this change was documented somewhere 👍

  18. jamesgpearce commented on Mar 20, 2025

    @jamesgpearce
    Contributor

    This is very surprising behavior. Worse that it's not documented nor optional.

    Especially since if you have already explicitly added the preload link tags to the head yourself and then boom... now you have two of them in the DOM.

    No framework; just a raw renderToStaticMarkup in React 19 will do this.

  19. talatkuyuk commented on Mar 23, 2025

    @talatkuyuk

    I agree with @jamesgpearce,

    I also encountered this issue while testing the parsing of a simple MDX content that only contains an image syntax.

    I saw <link rel="preload" as="image" href="image.png"/> in the result . Then, while digging where does it come from, then I dropped here.

    I just used a raw renderToStaticMarkup in react-dom@19, react-dom/server in my test. I confirm react-dom@18 doesn't have this behavior.

    Now, I want to know how to disable it using an option.

  20. errorx666 commented on May 19, 2025

    @errorx666

    I'm using renderToStaticMarkup to render HTML to a forum. It supports limited HTML, which does not include link tags. Any time I include an img tag, I get an unwanted link tag to go with it.

    It is surprising and problematic for me that it renders any markup I didn't explicitly specify, and worse, there's no obvious way to opt-out.

  21. brunolemos commented on Dec 22, 2025

    @brunolemos

    If you are calling renderToStaticMarkup outside a lifecycle method, you can use this instead:

    import { createRoot } from 'react-dom/client';
    import { flushSync } from 'react-dom';
    
    export function reactToString(element: React.ReactNode): string {
      const div = document.createElement('div');
      const root = createRoot(div);
    
      flushSync(() => {
        root.render(element);
      });
    
      return div.innerHTML;
    }

    If not, you can remove the link tag from the generated html, as a workaround:

    import { renderToStaticMarkup } from 'react-dom/server';
    
    export function reactToString(element: React.ReactNode): string {
      let html = renderToStaticMarkup(element);
    
      html = html.replace(
        /<link\s+rel="preload"\s*as="image"\s*href="[^"]+"\s*\/>/g,
        '',
      );
    
      return html;
    }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions