The Async Image Loop

When I built the Friends with Beer podcast website, I built it using Eleventy, a JavaScript based static site generator. It’s Node.js based, which is great because it gives great flexibility in adding functionality and in development and build script options. Eleventy has a lot of great features that combine to allow flexible customization quickly and easily, so much so that the documentation understates just how powerful it is. I’ve had more than a few discovery moments along the way where I realized how much more there is to this framework than I suspected.

One of the neat things about playing with modern static site generators is that so much of the web development community who use SSGs for performance reasons also think about things like image optimization and overall site performance and overhead. It’s through my experiments with Eleventy that I learned about modern image optimization techniques and the img srcset and picture tags. I’ve long had a gnawing sense of discontent at how I’ve handled images on this site and other sites.

In Eleventy, image optimization is easy to design in. Zach Leatherman, creator of Eleventy, built a plugin called eleventy-img which can generate different size and format versions of an image and return responsive html links for the image. In the past, I’ve considered generating smaller images which would then link to the original full-sized image. But HTML responsive image syntax is a much better way of doing things, so I gladly put the Eleventy Image plugin into action on Friends with Beer.

By default, Eleventy Image is made to run asynchronously, which makes sense given that it’s generating additional images for each image passed in. Normally this is fine, but async functions don’t work well inside a standard for loop. Looping on an async function can lead to weird results for obvious reasons, even if you think you’re awaiting the results properly. On Friends with Beer, the reason this matters is because I loop through the list of beer we’ve had on the podcast to generate episode beer lists and the full list for the beer page.

Episode Beer List

Beer Page beer list

The result was that on the beer page, the wrong images were showing up for any given beer. Recompiling changed which image showed up for which beer, as could be expected. Also the home page was throwing in duplicates of some sections of the page and eliminating some closing div tags, resulting in a disastrous mess.

Fortunately for me, Zach though of everything, and you can use the Eleventy Image plugin synchronously and still return the correct HTML for the image. So I did. And my site build times suddenly more than tripled.

Normally this wouldn’t matter, except at the time I was using rimraf to clean out the destination web directory before building in order to make sure that anything I removed in the source was also removed from the site. Eleventy and most (all?) other site generators don’t remove static files from the destination directory just because they’re no longer in the source and are not being compiled. The thought of the web directory sitting empty or incomplete for almost half a minute didn’t sit well with me at all, so I started rethinking this purge then build strategy. I decided the likelihood of me removing files other than while actively adding or modifying functionality was extremely low, so I no longer purge the destination directory before the site build.

Problem solved, right? Kind of, except that as time went on, the longer build times started driving me crazy during development as well. Every time I made a minor change, I had to wait 20+ seconds for the site to rebuild before I could see my change. Finally this past week while working on expanded beer information (thumbs up/down ratings, descriptions, etc) I had enough and decided to investigate how to shorten build times when using Eleventy Image. That’s when I ran across this:

An async function walks into a loop.

Turns out JavaScript has a concept of asynchronous loops. Instead of using a “for x in y” loop, the key is to use a “for let x of y” loop instead.

for (let beer of list) {
	if (!type || (beer.episodes.indexOf(episode) !== -1)) {
		beerList += `<div class=“beer${size === small ?  small-list : “”}”>
		<div class=“beer-image${type ?  beer- + type : “”}”>
		<a href=“/bottle/${beer.id}”>${await image(beer.image, {folder:beer, alt:beer.brand +   + beer.name, widths: site.respSizesBeer, sizes: site.beerImageSize})}</a></div>
		<div class=“beer-name”>
		<div class=“brewery”>${beer.brand}</div>
		<div><a href=“/bottle/${beer.id}”>${beer.name}</a></div>
		${!type ? `<div class=“beer-episode-name”>
		Episode${beer.episodes.length > 1 ? s : “”} 
		${beer.episodes.toString().replace(,, , )}</div>` : “”}
		</div>
		</div>`
	}
}

This allows the use of await with the image component, which in turn allows me to use the asynchronous version of the code inside that code.

Alternatively, you can use the array method .forEach() instead to iterate through an array with an async function handled correctly inside the loop. This way the callback events correctly get assigned to the correct index of the loop.

Because my shortcode that creates the beer list and which has the loop with the async image component call inside it is written in JavaScript, I can do this. There is no outer loop outside of this shortcode on either the beer page or my episode pages. The same is not true on the home page though.

The home page iterates through each episode and displays them all (with pagination, of course). As well, the latest episode also shows the list of beer for that episode. Even though it’s only the latest episode that does so on the index page, that means if I use the async beer list shortcode, I have it inside a nunjucks template language for loop (because it’s iterating the episodes). This does not work, and the home page still renders weird, the same as it used to the first time I tried using my async beer list generator.

Index page list of episodes

The end result is that for the index or home page, I use the synchronous versions of my beer list and image shortcodes, and for the beer page and episode pages, I use the async versions. My compile times are still almost identical to using async on all three types of pages, because there’s only one index page. It doesn’t gate things anything like the beer page or the collection of episode pages do when they’re calling Eleventy Image.

One thing you might be wondering is why I call Eleventy Image on all these pages instead of just the big beer list page, if the goal is to generate various versions of each image for using responsively. The answer is that I need the correct responsive html generated for me as well as the various images. The good news is that Eleventy Image is smart enough not to re-generate existing images, so it’s not trying to write the same images multiple times. In this case, it just generates the html for the links. Even so, the build times are still much longer doing this process synchronously, so even if you only expect to actually be generating a few new images per build, using the async functions greatly reduces the build times.

And finally, while writing this, I discovered that Nunjucks has an asyncEach loop. I may give that a try on my index page and see if I can dispense with using my synchronous shortcodes for generating the latest episode’s beer list on the home page.