Minimal reproduction of race condition bug in gatsby-plugin-image

This is a minimal reproduction of a bug I discovered in gatsby-image-plugin. Speficially, it's a race condition between the script in charge of hiding placeholder images when full-size images are fully loaded (this one) and pages that take very long to parse. Under certain conditions, this race condition causes the placeholder image generated by the plugin to remain on the page, "behind" the full-size image, instead of fading out.

What's going on?

The script linked above, which is appended to the <head> during SSR by the image plugin, binds a listener to the load event on <body>. This is a bubbling event, so the listener is written to return early unless the target of the event is a <GatsbyImage> (or <StaticImage>, both are affected). For the latter, the script is in charge of setting the CSS opacity of placeholder images to 0 when their full-size counterparts are fully loaded.

This <script> is of type="module", which also means it is implicitly deferred. Deferred scripts are only executed once the DOM has finished parsing.

If, for whatever reason, an image rendered by the Image plugin finishes loading before the DOM finishes parsing, then it will emit its load event before the listener mentioned above is ever bound. As a result, the script will never remove the placeholder for this image, and it will remain visible on the page.

To illustrate this, the Gatsby site in this repo has a single page where a <StaticImage> is followed by 50,000 empty <div>s. The image is loaded more quickly than the browser can parse the rest of the DOM. As a result of the race condition described above, the placeholder remains visible. (in order to dispel any concerns that the page is simply stalled due to the huge DOM, I included a little state-changing button to prove that the page is perfectly fine, although it may perform slowly on some machines).

To reproduce

  1. Run npm install
  2. Run npm run build
  3. Run npm run serve
  4. Browse to the served site and observe that the placeholder image does not disappear (since this is a race condition bug, you may or may not need to refresh a couple of times to "catch" it).