Rescuing Landing Pages from a mass placeholder-image-loading catastrophe

When you think about images and page load time, you typically think about large images — that is, “large” in terms of bytes, not necessarily dimensions — especially over mobile connections.

Developers don’t think much about image loading failures from a performance standpoint, assuming an image that isn’t available will return a 404 or 500 error immediately. Indeed, that’s true almost all the time.

So we don’t usually worry about image requests hanging for an extended period before failing, and almost never consider hidden images at all!

But ignoring rare-but-not-impossible cases is dangerous.

Take the client who was using placeholder images, via placeholder.com, throughout their Landing Page Templates:

<img src="https://placeholder.com/300x300">

On a good day, that returns the helpful image:

Those were placed inside Marketo editable areas that could be switched on and off using mktoBoolean variables.

But “on” and “off” are misnomers. In fact, the variable doesn’t switch the image off in any real way. You might have a <div> whose display style depends on the variable, like:

<div style="display: ${displayContainer1};">
  <img src="https://placeholder.com/300x300">
</div>

So you end up with either

<div style="display: block;">
  <img src="https://placeholder.com/300x300">
</div>

or

<div style="display: none;">
  <img src="https://placeholder.com/300x300">
</div>

when Marketo outputs the value.

But the image URL is still loaded in both of those cases. This is even true if the display property is set right on the <img> tag:

<img style="display:none;" src="https://placeholder.com/300x300">

And, as with all <img> tags, the page is not considered to have fully loaded until that image has either completely failed or completely succeeded. While it’s in process, the page is still loading.

If you’re waiting for load, you’re waiting for images

The fact that the load event won’t fire on the window object until all images are complete might not be a problem on your site. But it certainly was a problem for this client, since they used window.onload to trigger all the stuff that made the page usable (buttons clickable, forms submittable, etc.), e.g.:

<script>
  window.onload = function(){
    setUpCriticalPageBehaviors();
  };
</script>

Then the perfect storm happened: placeholder.com’s main site broke, so this URL no longer works:

https://placeholder.com/300x300

But instead of returning an error immediately, it took on the order of 7-10 seconds:

Before finally returning an HTTP 500 and a database error:

(Of course, that database error would never be seen by the end user because it’s in an <img> tag, but you can see it by opening the image URL in its own tab.)

So what happens? All of their Landing Pages — and I do mean all, since they had at least one placeholder in each — were taking ~10 seconds to be usable.

JavaScript — and a browser quirk — to the rescue

Leaping into action, we have 2 rules:

  1. We can’t find and remove those <img> tags on every one of hundreds of pages.
  2. We can add a <script> tag to the template and reapprove every page.

The solution is this code in the <head>:

document.addEventListener("DOMContentLoaded",function(e){
  const arrayify = getSelection.call.bind([].slice);
    
  arrayify(document.images)
    .filter(function(imgTag){
        let srcLoc = document.createElement("a");
        srcLoc.href = imgTag.src;
        return srcLoc.hostname == "placeholder.com";
    })
    .forEach(function(placeholderImgTag){
        placeholderImgTag.src = "";
    });
});

This works because

  1. DOMContentLoaded fires when the <img> tags have been parsed, but doesn’t wait for the image assets to be downloaded/decoded/error out.
  2. An <img> tag is considered complete if the src value is the empty string, including if you change the src to an empty string on-the-fly.

Coming soon...

A better way to hide unused modules, not display: none;, that doesn’t waste resources/impede performance.