Spec - noeldelgado/image-halt GitHub Wiki

image-halt

Preload images with an option to cancel the transfer if needed without stoping the window loading.

Introduction

Fetching images over the network is both slow and expensive. Browsers has a limit of how many connections per hostname can be established simultaneously. This configuration number may change anytime though.

Browser Connections per Hostname Max Connections
IE 9 6 35
IE 10 8 17
IE 11 13 17
Chrome 32 6 10
Chrome 34 6 10
Firefox 26 6 17
Firefox 27 6 17
Safari 7.0.1 6 17
Android 2.3 8 10
Android 4 6 17
Blackberry 7 7 9
Chrome Mobile 18 6 16
IEMobile 9 6 60

Source: May 28, 2015
http://www.browserscope.org/?category=network

Average of Connections per Hostname : 6.923076923076923

connections = [6,8,13,6,6,6,6,6,8,6,7,6,6]
sum = connections.reduce(function(c, n) {return c + n})
sum / connections.length

=> 6.923076923076923

Knowing the average number of parallel connections allowed by browsers (I will round it down to 6) it is worth knowing that if we rebase that number (e.g. say we are trying to download 20 images at the same time) we will get no warnings, errors or any other type of message regarding this issue, simply because the browser will handle the pool itself, so it's not big deal, the problem came when we are still downloading images that we no longer need, keeping useless connections open when we can be giving a better use to those connection to download more content the user may need right away.

TCP connection scheduler pseudo-code of a popular browser.

if (num_conn_open < max_conn) {
	if (idle_conn && !conn_expired()) {
		reuse_conn();
	}
	elseif {
		open_new_conn();
		close_expired_conn();
	}
}
elseif {
	open_new_conn();
	close_oldest_idle_conn();
}

Source: Web Content Delivery Book

So, if delivering content quickly is important for your program, you may want to benefit for closing useless connections.

Rationale

We can benefit for canceling image downloads if we no longer need those resources to be fetched, so we can have less memory being consumed and more available connections to fetch more data if needed.

Users will have to wait less time for new images to be displayed if they request new dynamic content and all the available connections per hostname were already in use by downloading images that the user no longer need.

Use Cases

  • Extract all urls from the image tags and start the image loading only when the user scrolls to a specific one.
  • Long page that dynamically loads images as users scroll through.
  • Downloading multiple images to display a dynamic image gallery, the user closes the gallery before all images got transferred and opens a new gallery resulting on requesting more new images to be fetch.
  • On Single Page Applications (SPA), downloading the images for a specific page, then the user request to view a new different page.

Problem

Need to cancel image transfers to have more parallels downloads available.

Constraints

  • network.http.max-connections and network.http.max-connections-per-server limit is different per browser.
  • Do not interrupt the current window loading window.stop().
  • Browser support: Google Chrome, Firefox, Safari, IE9+

Background

New Image Object

Creating image objects and relying on the onload and onerror handlers we can benefit if the image is already in cache, it won't send another request to the server.

Good:

  • It allow us to cancel the image loading by removing the src attribute or by changing the src attribute
  • Async fetching
  • No limited by the same origin policy

Bad: Safari does not stop previous connections by changing the src attribute.


window.stop / document.execCommand("Stop")

We can use window.stop to stop window loading.

Bad: This will stop large images, new windows, and other objects whose loading is deferred.


Load on Iframe

Creating an iframe with an about:blank source and append the image on the iframe to be loaded.

Good:

  • Calling iframe.contentWindow.stop() will successfully stop the image download.
  • IE does not recognize the stop method, but it has its own document.execCommand("Stop"); which produce the same results.
  • Every browser uses Browser Cache by default for (images, JavaScript, CSS), unless you change the image's HTTP Headers. more info on HTTP caching. That means for us that downloading the image inside an iframe and then appending in on the current window will not cause a new requests but will be loaded from cache.

Bad:

  • Require an extra iframe element to be created per instance
  • Appending the image on the main window will cause a new download (if cache is disabled)

Preloading with Ajax

Good:

  • XMLHttpRequest can be aborted
  • Async fetching

Bad:

  • Limite by the same origin policy
  • Will produce a new connection per every request (even if the resource was fetched before)

Goals

  • Provide a way to cancel data transfer for registered images while they are not yet fully fetched.
  • Provide a way to get notified when a registered image gets downloaded or an error happened while downloading it.

Hypothesis

If we have a way to cancel image downloads then we can have more connections per hostname available to made new requests right away if needed.

Assumptions

  • Browsers caches images requests, so if we try to fetch an already loaded image it will serve it from the cache and will not perform a new request.

Solution Proposal

Loading

Create new image objects and listen the onload and onerror handlers, using this approach give us the benefit that if the image is already in cache, it won't send another request to the server.

Canceling

Use a blank data:uri string to avoid removing the src attribute, having an empty src seems to be problematic Empty Image src can Destroy your Site - @slicknet - November 30, 2009, so instead of removing the src attribute or emptying it, we replace it with a small data:uri string, which will cause no extra HTTP request. The downside is that this will fire the image load event, which in this case is not what we want, so before replacing the src we unbind the image event listeners.

Changing the src to a data:uri string string will cancel the previous image download on the following browsers: Chrome, Firefox, Opera and IE.

Safari does not honor this behavior, so for Safari specifically we will use a different approach. Creating an iframe and loading the image inside the iframe will allow us to call window.stop on the iframe content to successfully close the connection.

This can be used for the blank data:uri Base64 Encode of 1x1px Transparent GIF

Theory of Operation

  1. Need to load new images to display on the screen.
  2. A new instance per image is created and we set a callback to be run when the image successfully loads or an error occurs.
  3. If the callback is run we check if it was an error and handle it.
  4. If the callback is run and is not an error we display the image.
  5. If new content is requested and we no longer need to wait for these images to be loaded, we abort all the unfinished instances.

Functional Specification

Methods

abort (public)

We need a public method that we call for every instance created. This method will be in charge of canceling the registered image download.

I found out that on Chrome, Firefox, Opera and even IE changing the src attribute to a small data:uri for the in-memory object image will stop the download. However Safari will keep the transfer until the download is fulfill.

Changing the src attribute is the most simple solution, so we can use that approach by default for any browser and implement a different approach for Safari.

For Safari, creating an iframe, appending the image to the iframe and calling contentWindow.stop() on the iframe will do the trick.

load (public)

To start listening for an instance to load or to fail we should call the load method. This will bind the unload and onerror events and update the image source.

Technical Specification

Because the simple approach of changing the src attribute from the in-memory image object will work for most browsers, I decided to use a strategy of module inclusion. This way we have a default behavior shared by those browsers and a different object that will be included just in case the browser detected is Safari, which will have a different approach. We can use the navigator.userAgent string to determine which behavior to include.

Methods

discoverImplementation (protected, static)

if ( /^((?!chrome).)*safari/i.test(window.navigator.userAgent) )
	this._implementation = window.MainClassSafari
else
	this._implementation = window.MainClassDefault

This will determine which behavior our main class should include.

MainClass.discoverImplementation()
include( MainClass._implementation )

The include function there is charge of merging the properties from the passed Object to the prototype of our MainClass.

abort

var BLANK = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==';

// so we do not get notified for the load of the data:uri
unbindImageEvents(); 

// Default approach
image.setAttribute('src', BLANK);

// Approach for Safari
iframe.contentWindow.stop();

load

// Default approach
image.setAttribute('src', passedImageSource);

// Safari approach
iframe = createElement('iframe')
iframe.src = 'about:blank'
iframe display none
body append iframe
iframe append image

image.setAttribute('src', passedImageSource);

Conclusion

Being able to halt image fetching over the network allow us to have more available connections to fetch more data if needed and to use less memory.

⚠️ **GitHub.com Fallback** ⚠️