background

The network environment is always diverse and complex. An image may fail to load or take a long time to load due to poor network condition, or fail to load due to insufficient permissions or non-existent resources. All these will lead to poor user experience, so we need a solution to solve the problem of abnormal image loading.

<img> Load error solution

Inline event

Use inline events directly on the IMG tag to handle image load failures, but this approach is so intrusive that you might miss something.

<img src='xxxxx' onerror="this.src = 'default.png'">
Copy the code

Global IMG adds events

The first option was too intrusive, so we converged the entries and added error handling events uniformly for all IMG tags.

const imgs = document.getElementsByTagName('img')
Array.prototype.forEach.call(imgs, img= > {
    img.addEventListener('error'.e= > {
        e.target.src = 'default.png'})})Copy the code

Capture with error events

Adding event handlers for each IMG is still a bit expensive, as we know that events typically go through three stages:

  • Capture phase
  • In the target stage
  • Bubbling phase

As described in the MDN documentation:

When a resource (such as an <img> or <script>) fails to load, an error event using interface Event is fired at the element that initiated the load, and the onerror() handler on the element is invoked. These error events do not bubble up to window, but can be handled with a EventTarget.addEventListener configured with useCapture set to true.

We know that the errors of img and SRCIPT tags do not bubble up, but go through the capture phase and the target phase. The first two scenarios use event functions that are fired in the target phase. This time we intercept and fire functions in the capture phase to reduce the performance cost.

document.addEventListener(
    'error'.e= > {
        let target = e.target
        const tagName = target.tagName || ' '
        if (tagName.toLowerCase = 'img') {
            target.src = 'default.png'
        }
        target = null
    },
    true
)
Copy the code

Replace the optimal solution of SRC mode

The above scheme has two disadvantages:

  • If the loading fails due to poor network, then loading the default image will also fail with a high probability, so it will fall into an infinite loop.
  • If the load fails due to network fluctuation, the image may load successfully after retry.

So we can add an additional data-retry-times count attribute to each IMG tag and use the Base64 image as the default pocket when the maximum number of retries is exceeded.

document.addEventListener(
    'error'.e= > {
        let target = e.target
        const tagName = target.tagName || ' '
        const curTimes = Number(target.dataset.retryTimes) || 0
        if (tagName.toLowerCase() === 'img') {
            if (curTimes >= 3) {
                target.src = 'data:image/png; base64,xxxxxx'
            } else {
                target.dataset.retryTimes = curTimes + 1
                target.src = target.src
            }
        }
        target = null
    },
    true
)
Copy the code

The optimal solution for CSS processing

The above solution uses the substitution of SRC to show the bottom pocket graph. This solution has a flaw:

  • The original resource link cannot be obtained from the tag (although you can hack it by adding the data-xxx attribute).

A better way is to use CSS pseudo-elements ::before and :: After to override the original elements and display the base64 image directly.

The CSS style is as follows:

img.error {
  display: inline-block;
  transform: scale(1);
  content: ' ';
  color: transparent;
}
img.error::before {
  content: ' ';
  position: absolute;
  left: 0; top: 0;
  width: 100%; height: 100%;
  background: #f5f5f5 url(data:image/png; base64,xxxxxx) no-repeat center / 50% 50%;
}
img.error::after {
  content: attr(alt);
  position: absolute;
  left: 0; bottom: 0;
  width: 100%;
  line-height: 2;
  background-color: rgba(0.0.0.5);
  color: white;
  font-size: 12px;
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
Copy the code

The JS code is as follows:

document.addEventListener(
    'error'.e= > {
        let target = e.target
        const tagName = target.tagName || ' '
        const curTimes = Number(target.dataset.retryTimes) || 0
        if (tagName.toLowerCase() === 'img') {
            if (curTimes >= 3) {
                target.classList.remove('error')
                target.classList.add('error')}else {
                target.dataset.retryTimes = curTimes + 1
                target.src = target.src
            }
        }
        target = null
    },
    true
)
Copy the code

<img> Load timeout solution

At present, most applications access CDN to speed up resource requests. However, CDN has incomplete node coverage, which results in TIMEOUT of DNS query. In this case, Domain switching may be successful.

Sniffer switch Domain(CNAME)

We can use the method of sniffing to test whether the Domain provided by CDN can be accessed normally. If it fails or times out, the Domain can be switched to accessible in time. A few points to note:

  • To prevent sniffing the image cache, time stamps need to be added to keep things fresh
  • Image Image loading has no timeout mechanism. Use setTimeout to simulate timeout
// Prevent sniffing images from being cached and add timestamps to keep them fresh
export const imgUri = `/img/xxxxx? timestamp=The ${Date.now()}The ${Math.random()}`;

export const originDomain = 'https://sf6-xxxx.xxxx.com'

// The delivery mode can be configured
export const cdnDomains = [
  'https://sf1-xxxx.xxxx.com'.'https://sf3-xxxx.xxxx.com'.'https://sf9-xxxx.xxxx.com',];export const validateImageUrl = (url: string) = > {
  return new Promise<string>((resolve, reject) = > {
    const img = new Image();
    img.onload = () = > {
      resolve(url);
    };
    img.onerror = (e: string | Event) = > {
      reject(e);
    };
    // The promise state is immutable. Use setTimeout to simulate a timeout
    const timer = setTimeout(() = > {
      clearTimeout(timer);
      reject(new Error('Image Load Timeout'));
    }, 10000);
    img.src = url;
  });
};

export const setCDNDomain = () = > {
  const cdnLoop = () = > {
    return Promise.race(
      cdnDomains.map((domain: string) = > validateImageUrl(domain + imgUri)),
    ).then(url= > {
      window.shouldReplaceDomain = true;
      const urlHost = url.split('/') [2];
      window.replaceDomain = urlHost;
    });
  };

  return validateImageUrl(`${originDomain}${imgUri}`)
    .then(() = > {
      window.shouldReplaceDomain = false;
      window.replaceDomain = ' ';
    })
    .catch(() = > {
      return cdnLoop();
    });
};

/ / replace the URL
export const replaceImgDomain = (src: string) = > {
  if (src && window.shouldReplaceDomain && window.replaceDomain) {
    return src.replace(originDomain.split('/') [2].window.replaceDomain);
  }
  return src;
};
Copy the code

Server Delivering Domain(CNAME)

This scheme requires the cooperation of the background students. The background judges the current available Domain and returns it.

getUsefulDomain()
    .then(e= > {
        window.imgDomain = e.data.imgDomain || ' '
    })
Copy the code

Background-image loading exception solution

In practice, background images will also fail to load, and usually these elements do not have error events, so there is no way to catch error events. At this point you can use dispatchEvent, which also has a capture phase, as described in the MDN documentation:

Dispatches an Event at the specified EventTarget, (synchronously) invoking the affected EventListeners in the appropriate order. The normal event processing rules (including the capturing and optional bubbling phase) also apply to events dispatched manually with dispatchEvent().

As you can see the support is still ok, we first need to define a custom event and initialize it, fire the custom event when the background image fails to load, and finally catch the event on top and execute the event function.

Custom events

There are two ways to customize events:

  • CreateEvent () and initEvent() are used, but according to the MDN documentation, the initEvent method has been removed from the browser standard and is not safe, but is highly supported.

  • The new Event() method was used, but slightly less popular

Using the second as an example, create a custom event according to the usage of the MDN document:

const event = new Event('bgImgError')
Copy the code

Sniff the load

Sniff the picture resource using the methods defined earlier.

validateImageUrl('xxx.png')
    .catch(e= > {
        let ele = document.getElementById('bg-img')
        if (ele) {
            ele.dispatchEvent('bgImgError')
        }
        ele = null
    })
Copy the code

Add Event capture

document.addEventListener(
    'bgImgError'.e= > {
        e.target.style.backgroundImage = "url(data:image/png; base64,xxxxxx)"
    },
    true
)
Copy the code

The related documents

  • Summary of CDN and DNS knowledge
  • Best practices for CSS style handling after image loading failure zhang Xinxu – Xin Space – Xin Life
  • How does “front-end Advance” gracefully handle image anomalies