Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with: Kirsty Lansdell

Preloading Images In AngularJS With Promises

By Ben Nadel on

When I first started using AngularJS, I was so concerned with doing things the, "Angular Way," that I ended up making some poor choices. Most noticeably was the way in which I tried to preload images in my application. Thinking only of images as "DOM" (Document Object Model) elements, I delegated all of my preloading to the rendered HTML. But, the reality is, Image objects can exist without being attached to the DOM. And, when you start thinking about them as containers for asynchronous data loading (much like AJAX), preloading images in AngularJS becomes a whole lot easier.


 
 
 

 
  
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

In AngularJS, your Controllers and your Services are not supposed to know anything about the DOM (Document Object Model) or about your HTML. Their only role is to model the business logic and define behaviors for your application. If you need to do something with the DOM, you're supposed to use a View or a Directive.

As such, if you need to do something with Images, your first thought is probably that they are HTML IMG tags, and, therefore, you need to isolate image loading in a View or a Directive. But this is an over-simplified understanding of images. If you break the image lifecycle into two parts - image loading and image rendering - you can start to see that part of the image lifecycle is about the data model and, therefore, can be handled by either a Controller or, more appropriately, by a Service.

In the following demo, I am using AngularJS to render an ngRepeat list of images. But, before I execute the rendering (via Directive), I'm preloading the image binaries using a Preload service. The Preload service takes an array of source values and returns a promise. The promise is resolved when all of the image binaries have been preloaded.

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Preloading Images In AngularJS With Promises
  • </title>
  • </head>
  • <body ng-controller="AppController">
  •  
  • <h1>
  • Preloading Images In AngularJS With Promises
  • </h1>
  •  
  • <div ng-switch="isLoading">
  •  
  • <!-- BEGIN: Loading View. -->
  • <p ng-switch-when="true">
  •  
  • Your images are being loaded... {{ percentLoaded }}%
  •  
  • </p>
  • <!-- END: Loading View. -->
  •  
  • <!-- BEGIN: Results View. -->
  • <div
  • ng-switch-when="false"
  • ng-switch="isSuccessful">
  •  
  • <p ng-switch-when="true">
  •  
  • <img
  • ng-repeat="src in imageLocations"
  • ng-src="{{ src }}"
  • style="width: 100px ; margin-right: 10px ;"
  • />
  •  
  • </p>
  •  
  • <p ng-switch-when="false">
  •  
  • <strong>Oops</strong>: One of your images failed to load :(
  •  
  • </p>
  •  
  • </div>
  • <!-- END: Results View. -->
  •  
  • </div>
  •  
  •  
  • <!-- Load scripts. -->
  • <script type="text/javascript" src="../../vendor/jquery/jquery-2.0.3.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs/angular-1.2.min.js"></script>
  • <script type="text/javascript">
  •  
  • // Create an application module for our demo.
  • var app = angular.module( "Demo", [] );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I control the root of the application.
  • app.controller(
  • "AppController",
  • function( $scope, preloader ) {
  •  
  • // I keep track of the state of the loading images.
  • $scope.isLoading = true;
  • $scope.isSuccessful = false;
  • $scope.percentLoaded = 0;
  •  
  • // I am the image SRC values to preload and display./
  • // --
  • // NOTE: "cache" attribute is to prevent images from caching in the
  • // browser (for the sake of the demo).
  • $scope.imageLocations = [
  • ( "./ahhh.jpg?v=1&cache=" + ( new Date() ).getTime() ),
  • ( "./ahhh.jpg?v=2&cache=" + ( new Date() ).getTime() ),
  • ( "./ahhh.jpg?v=3&cache=" + ( new Date() ).getTime() ),
  • ( "./ahhh.jpg?v=4&cache=" + ( new Date() ).getTime() ),
  • ( "./ahhh.jpg?v=5&cache=" + ( new Date() ).getTime() ),
  • ( "./ahhh.jpg?v=6&cache=" + ( new Date() ).getTime() ),
  • ( "./ahhh.jpg?v=7&cache=" + ( new Date() ).getTime() ),
  • ( "./ahhh.jpg?v=8&cache=" + ( new Date() ).getTime() ),
  • ( "./ahhh.jpg?v=9&cache=" + ( new Date() ).getTime() ),
  • ( "./ahhh.jpg?v=10&cache=" + ( new Date() ).getTime() ),
  • ( "./ahhh.jpg?v=11&cache=" + ( new Date() ).getTime() ),
  • ( "./ahhh.jpg?v=12&cache=" + ( new Date() ).getTime() ),
  • ( "./ahhh.jpg?v=13&cache=" + ( new Date() ).getTime() ),
  • ( "./ahhh.jpg?v=14&cache=" + ( new Date() ).getTime() ),
  • ( "./ahhh.jpg?v=15&cache=" + ( new Date() ).getTime() ),
  • ];
  •  
  • // Preload the images; then, update display when returned.
  • preloader.preloadImages( $scope.imageLocations ).then(
  • function handleResolve( imageLocations ) {
  •  
  • // Loading was successful.
  • $scope.isLoading = false;
  • $scope.isSuccessful = true;
  •  
  • console.info( "Preload Successful" );
  •  
  • },
  • function handleReject( imageLocation ) {
  •  
  • // Loading failed on at least one image.
  • $scope.isLoading = false;
  • $scope.isSuccessful = false;
  •  
  • console.error( "Image Failed", imageLocation );
  • console.info( "Preload Failure" );
  •  
  • },
  • function handleNotify( event ) {
  •  
  • $scope.percentLoaded = event.percent;
  •  
  • console.info( "Percent loaded:", event.percent );
  •  
  • }
  • );
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I provide a utility class for preloading image objects.
  • app.factory(
  • "preloader",
  • function( $q, $rootScope ) {
  •  
  • // I manage the preloading of image objects. Accepts an array of image URLs.
  • function Preloader( imageLocations ) {
  •  
  • // I am the image SRC values to preload.
  • this.imageLocations = imageLocations;
  •  
  • // As the images load, we'll need to keep track of the load/error
  • // counts when announing the progress on the loading.
  • this.imageCount = this.imageLocations.length;
  • this.loadCount = 0;
  • this.errorCount = 0;
  •  
  • // I am the possible states that the preloader can be in.
  • this.states = {
  • PENDING: 1,
  • LOADING: 2,
  • RESOLVED: 3,
  • REJECTED: 4
  • };
  •  
  • // I keep track of the current state of the preloader.
  • this.state = this.states.PENDING;
  •  
  • // When loading the images, a promise will be returned to indicate
  • // when the loading has completed (and / or progressed).
  • this.deferred = $q.defer();
  • this.promise = this.deferred.promise;
  •  
  • }
  •  
  •  
  • // ---
  • // STATIC METHODS.
  • // ---
  •  
  •  
  • // I reload the given images [Array] and return a promise. The promise
  • // will be resolved with the array of image locations.
  • Preloader.preloadImages = function( imageLocations ) {
  •  
  • var preloader = new Preloader( imageLocations );
  •  
  • return( preloader.load() );
  •  
  • };
  •  
  •  
  • // ---
  • // INSTANCE METHODS.
  • // ---
  •  
  •  
  • Preloader.prototype = {
  •  
  • // Best practice for "instnceof" operator.
  • constructor: Preloader,
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I determine if the preloader has started loading images yet.
  • isInitiated: function isInitiated() {
  •  
  • return( this.state !== this.states.PENDING );
  •  
  • },
  •  
  •  
  • // I determine if the preloader has failed to load all of the images.
  • isRejected: function isRejected() {
  •  
  • return( this.state === this.states.REJECTED );
  •  
  • },
  •  
  •  
  • // I determine if the preloader has successfully loaded all of the images.
  • isResolved: function isResolved() {
  •  
  • return( this.state === this.states.RESOLVED );
  •  
  • },
  •  
  •  
  • // I initiate the preload of the images. Returns a promise.
  • load: function load() {
  •  
  • // If the images are already loading, return the existing promise.
  • if ( this.isInitiated() ) {
  •  
  • return( this.promise );
  •  
  • }
  •  
  • this.state = this.states.LOADING;
  •  
  • for ( var i = 0 ; i < this.imageCount ; i++ ) {
  •  
  • this.loadImageLocation( this.imageLocations[ i ] );
  •  
  • }
  •  
  • // Return the deferred promise for the load event.
  • return( this.promise );
  •  
  • },
  •  
  •  
  • // ---
  • // PRIVATE METHODS.
  • // ---
  •  
  •  
  • // I handle the load-failure of the given image location.
  • handleImageError: function handleImageError( imageLocation ) {
  •  
  • this.errorCount++;
  •  
  • // If the preload action has already failed, ignore further action.
  • if ( this.isRejected() ) {
  •  
  • return;
  •  
  • }
  •  
  • this.state = this.states.REJECTED;
  •  
  • this.deferred.reject( imageLocation );
  •  
  • },
  •  
  •  
  • // I handle the load-success of the given image location.
  • handleImageLoad: function handleImageLoad( imageLocation ) {
  •  
  • this.loadCount++;
  •  
  • // If the preload action has already failed, ignore further action.
  • if ( this.isRejected() ) {
  •  
  • return;
  •  
  • }
  •  
  • // Notify the progress of the overall deferred. This is different
  • // than Resolving the deferred - you can call notify many times
  • // before the ultimate resolution (or rejection) of the deferred.
  • this.deferred.notify({
  • percent: Math.ceil( this.loadCount / this.imageCount * 100 ),
  • imageLocation: imageLocation
  • });
  •  
  • // If all of the images have loaded, we can resolve the deferred
  • // value that we returned to the calling context.
  • if ( this.loadCount === this.imageCount ) {
  •  
  • this.state = this.states.RESOLVED;
  •  
  • this.deferred.resolve( this.imageLocations );
  •  
  • }
  •  
  • },
  •  
  •  
  • // I load the given image location and then wire the load / error
  • // events back into the preloader instance.
  • // --
  • // NOTE: The load/error events trigger a $digest.
  • loadImageLocation: function loadImageLocation( imageLocation ) {
  •  
  • var preloader = this;
  •  
  • // When it comes to creating the image object, it is critical that
  • // we bind the event handlers BEFORE we actually set the image
  • // source. Failure to do so will prevent the events from proper
  • // triggering in some browsers.
  • var image = $( new Image() )
  • .load(
  • function( event ) {
  •  
  • // Since the load event is asynchronous, we have to
  • // tell AngularJS that something changed.
  • $rootScope.$apply(
  • function() {
  •  
  • preloader.handleImageLoad( event.target.src );
  •  
  • // Clean up object reference to help with the
  • // garbage collection in the closure.
  • preloader = image = event = null;
  •  
  • }
  • );
  •  
  • }
  • )
  • .error(
  • function( event ) {
  •  
  • // Since the load event is asynchronous, we have to
  • // tell AngularJS that something changed.
  • $rootScope.$apply(
  • function() {
  •  
  • preloader.handleImageError( event.target.src );
  •  
  • // Clean up object reference to help with the
  • // garbage collection in the closure.
  • preloader = image = event = null;
  •  
  • }
  • );
  •  
  • }
  • )
  • .prop( "src", imageLocation )
  • ;
  •  
  • }
  •  
  • };
  •  
  •  
  • // Return the factory instance.
  • return( Preloader );
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

Trying to preload images directly in your HTML is going to make your HTML harder to read, understand, and maintain. Leave the rendering of images up to your HTML; but, if you need to preload images in your AngularJS application, do that in your business logic - the same way you would request non-binary JSON (JavaScript Object Notation) data from the server.

Tweet This Groovy post by @BenNadel - Preloading Images In AngularJS With Promises Thanks my man — you rock the party that rocks the body!



Reader Comments

Very neat approach, concisely and logically written, well commented. Nice one - thanks!

I would consider using ng-cloak to stop the flash of "Oops: One of your images failed to load :(" during the demo - admittedly not part of what you're demonstrating here, but makes for a neater experience nonetheless.

Reply to this Comment

Interesting idea, but wouldn't it make more sense to show images as they are loaded (rather than waiting for all to finish) and show images if at least one loads instead of showing none if one (or more) fails?

Reply to this Comment

@Steve,

Good suggestion. When I load everything on my local machine, it all happens so fast, I sometimes forget about that stuff. Then, when I upload to GitHub, I forget about the real latency.

Reply to this Comment

@Royce,

I think it depends on the context. At work, we have an even slightly different scenario - we _partially_ preload the set of images _and_ we show the ones that failed to load (they show up as blank). So, each preload idea is going to be a bit different.

I think the example could be update to return the array or source locations that preloaded successfully, even if one or more failed. Especially since we don't actually stop the preloading (behind the scenes).

Reply to this Comment

Ben,

Thanks for sharing this approach. I am curious as to whether or not you considered using $http.get to obtain the preloading of the images. If so, can you explain why what you are doing is better ?

Thanks

Reply to this Comment

Dear Ben,

I am fairly new to AngularJS and just want to initially mention that your articles are a great inspiration and a reliable source of knowledge. Please keep up the good work!

Inspired by your approach I experimented with a slightly different angle for a solution. As I do experiment a lot with photography, I know that if images would be loaded with the approach in your article, a lot of users would leave the page as the loading of, let's say 10 high resolution images in a gallery, would present a blank page for a considerable amount of time before the images would be displayed.

Without including jQuery at all, it is possible to write a very compact attribute directive that emits (upwards) an event which is triggered on the images load event (or a failure to load as well). When the parent controller (the preloader) observes the emitted event, it appends the next image for loading. The result is a preloader that loads the images in sequence (instead of concurrently). The nice effect of this is that the first image in the sequence of pictures is loaded and displayed very fast, and while the visitor observes it, the rest of the images are loaded in the background. It simply provides a strong feeling of visiting a fast page.

I don't know at all if this is an "acceptable" Angular way of doing thing...but still...I would not have been able to write something like that without learning a lot from your articles. I will keep on following your admirable work!

Best regards,

Fedde

Reply to this Comment

@Federico, I think that you could get that sort of feature out of this tool. This service allows you to request any set of images and gives you a promise for them. To get what you want, you probably just need to request your images in chunks. When one chunk of images completes, you start the next chuck. Perhaps break your ten images into sets of 1, 1, 2, 2, and 4. This way your first image is loaded as fast as possible; the second is also loaded as fast as possible. The remaining images are loaded in chunks which may load the entire set faster but the user won't notice because they are still gazing at the first two images you made sure to load super fast. The exact configuration of how you break the overall list of images into sets would depend on a lot of factors and you'd probably have to play with it to get the right feel for your site.

Reply to this Comment

Thanks @squid314. I more or less managed to write what you describe, but in a simpler way. For the landing page I basically preload three random images that slowly blend with eachother. So the important thing there is to manage to load the pics linearly. For my galleries, which can have around 8 to 10 pics, I simply preload the two next images to have something cached ahead. When the user steps forward in the gallery a new future image is loaded, giving the impression that the transitions between the images is extremely "quick".

I just wanted to write all the parts myself to learn how promises really work. That's the best way to learn. But thanks again for your input!

Reply to this Comment

Hey, Thank you.

Could you please elaborate on the triggering of load? (Line 317)

My images are GET twice.

Reply to this Comment

@Mp,

I'm not actually invoke the load() method there, I'm binding to the load-event. The callback that I pass into the .load() method will be invoked when / if the image loads successfully.

When you say your images are being GET twice, take look at the network activity and check to see if one of them says "(from cache)". I have seen some browsers where a hard-refresh of the page will show 2 requests - one from cache and one from the live server.

Reply to this Comment

@Keith,

The reason that I'm doing an actual image load instead of an $http.get is that I don't believe the $http.get will actually populate the browser's native cache. By loading an actual Image() object, the binary of the image is stored in the cache; then, when I got to render the image (with the *same* source URL) on the page, the browser can pull it right from the cache instead of making a request to the server.

Reply to this Comment

@Federico, @squid,

Sounds like you got it sorted out. Promises are really awesome! But, they are a lot wrap your head around. If you are really interested in them, one article that I have bookmarked that I keep going back to is on Promise anti-patterns:

http://taoofcode.net/promise-anti-patterns/

It's really helpful to see how one might fix problematic Promises so that you can actually see how nice a clean solution can be.

But, they are definitely complex!

And, @Federico, thank you so much for the kind words! I hope to keep rocking out more AngularJS goodness!

Reply to this Comment

@Ben, Thank you again.

Both are 200. Then both are 304 after first load.

This occurs with Chrome 34/Safari 7. Not FF (latest). It's strange. The initiators are different for both GETs. jQuery (2.0.3) for the first. Angular-animate (1.2.0) for the second. (I'm also using UI-Router v0.2.10)

Reply to this Comment

@Mp,

I don't have any experience with the the Animate module in AngularJS yet. I'm not sure why it would appear to be initiating an image request. Something sounds a bit funky there. Sorry I can't be more help.

Reply to this Comment

@Federico,

I actually had to do something similar a while back (before I experimenting with preloading images using a server). What I did in that case was take my collection of img SRC values and, in my Controller, sort them based on the order I wanted them to load:

images = sortImagesByRelevance( images );

... then, in the HTML, I used an ng-repeat to render them AND I had a directive on each of them that would announce a "loaded" event like you are describing:

<img ng-repeat="image in images" ng-src="{{ image.src }}" bn-image-event />

The key point here is that since I ordered them in the Controller, they appear in the DOM in the order that I "hope" they load. At this point, I kind of just let the browser handle the loading/blocking of HTTP requests. This doesn't guarantee that the first ones will be the first to *finish* loading; but they will initiate loading in the order that I want.

This would announce an event for each loaded image that my Controller would listen for. And, for each image that it loaded, it looked at the SRC and checked it against the images that I wanted to preload; and, when the target images were all preloaded, I would do something.

It ends up spreading the logic around, which I wasn't thrilled with; but it worked out fairly well and I didn't have to manage the loading in groups of images.

Reply to this Comment

@Ben,

Thanks again for the detailed reply!

I made a variant of your logic in my code, but implemented a different strategy of it that in my case guarantees that the images are loaded in the order I want.

My controller contains two lists. The first list, the "loading" list, is initially fetched from the server. This list contains a list of image URLs already sorted on some kind of order of relevance.

The second list is the list that ng-repeat loops through. Originally this list is empty and I start to populate it with the first image from my "loading" list. I only append a new element to this list once I get a proper loading event (or a loading error, in which case I skip over it). In this manner I never have multiple images loading at the same time, as what I want is to ensure that my images are loaded in the proper order without having them compete for network capacity. This is a key requirement in for example mobile terminals that communicate over potentially loaded radio networks.

Just as you did, I had to write an attribute directive to generate an event to flag the loading of an individual image. As my image controller is attached to an element that is the parent of my IMG nodes, it can listen for this event to know when to append a new image to the controller's list of images. An just as you experienced, I initially did not like my logic to be divided in this manner. After a while though, the solution matured within me and today I find it elegant for the simple reason that it results in very little code. =)

Once again, your comments are highly valued!

Best regards!

@Ben,

Reply to this Comment

@Ben

Thanks for the great article! Good way to learn the power of promises and creative usage in AngularJS.

I noticed your way of using an Image object bypasses some CORS limitations you would have using $http, so that's great!

I also looked at a way to implement your solution without the Jquery dependency. So I came up with:

var image = angular.element(new Image()).bind('load', ... ).bind('error', ...). attr('src', imageLocation)

I think it does the same eventually, right?

Thanks and keep it up!

Reply to this Comment

@Adriaan,

Thank you for the kind words! And yes, I believe that what you have there does the same thing. The important point (from what I've read) is that the event-handlers have to be bound *before* the SRC value is set, which is what your chaining provides. jQuery is pretty awesome that way!

Reply to this Comment

Dear Ben:

Your initial ideas for this post inspired me to completely rewrite my homepage. After exchanging some ideas in the thread above with you, I am very happy with the final results.

Now a "linear" preloading strategy loads the images of my frontpage image slider and my galleries.

http://www.farodyne.com

The best part about the web is people who share. Your blog rocks! =)

Reply to this Comment

This is great Ben - nice work! Any ideas on loading other datatypes - specifically video? I've got a few video backgrounds and it'd be nice to be able to preload them along with the images.

Reply to this Comment

@Ben,
I really like your different approaches on problems. But I have some doubt on this one. Now, I didn't read the whole code and I might be wrong, but aren't you forcing 'N' $digest cycles for 'N' images load?

Reply to this Comment

Awesome post and really high quality javascript/angular. I truly say that I've learned not only from the blog subject but also from your coding style.

Reply to this Comment

Great post. I've learned a lot from it. For last two days I'm trying to figure out how to test this "preloader" factory with Jasmine. I am not sure what and how to mock real image loading here. Could it be a problem that load function is on Preloader instance?

Reply to this Comment

I've found that some browsers (e.g. Chrome) will garbage collect your preloaded images if you haven't used them after a few seconds. To get around this, make sure you keep a reference to the Image objects you create. (And make sure you eventually release them too.)

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.