Hero Image Custom Metrics
The takeaways from this post are:
- If your website has a hero image, make sure it loads and renders as early as possible. Many designs that feature a hero image suffer from HID (Hero Image Delay) mostly due to blocking scripts and stylesheets.
- You should add custom metrics to your website to make sure you know how quickly (or slowly) important content gets displayed. I’ll describe a new technique for measuring when images are rendered so you can track that as a custom metric.
HID (Hero Image Delay)
The size of websites is growing. The average website contains over 2 MB of downloaded content. Of that, 1.3 MB (65%) is images. Many websites use images as a major design element in the page – these are called hero images. Because these hero images are critical design elements, it’s important that they render quickly, and yet often hero images load too late frequently getting pre-empted by less critical resources on the page.
Popular websites that use hero images include Jawbone, Homeaway, and Airbnb. Using SpeedCurve’s Responsive Design Dashboard, we can see how their hero images load over time across different screen sizes.
Jawbone
Jawbone’s site features an image of a woman in a sweater wearing the Up fitness tracker. While this picture is stunning, it takes 2.5-3.5 seconds before it’s displayed.
Often we think that’s the price we have to pay for rich images like this – they simply take longer to download. But investigating further we find that’s often not the cause of the delayed rendering. Looking at the waterfall chart for Jawbone in Figure 2, we see that the image loaded in ~700 ms. (Look for the white URL ending in “sweater-2000.v2.jpg”.)
So why didn’t the hero image get rendered until almost 2600 ms? First, it’s referenced as a background image in a stylesheet. That means the browser’s preloader can’t find it for early downloading and the browser isn’t even aware of the image’s URL until after the stylesheet is downloaded and its rules are parsed and applied to the page. All of this means the image doesn’t even start downloading until ~650 ms after the HTML document arrives. Even after the image finishes downloading it’s blocked from rendering for ~550 ms by the scripts in the page.
Homeaway
Homeaway’s entire front page is one big hero image. Without the image, the page looks broken with a gray background and a floating form. For larger screen sizes, the hero image isn’t shown for over 2.5 seconds.
The Homeaway waterfall in Figure 4 shows that the (initial, low-res version of the) hero image loads early and quickly (request #9). But then it’s blocked from rendering for over 2 seconds by other scripts in the page.
Airbnb
Similar to Homeaway, Airbnb uses a beautiful hero image to cover nearly the entire front page. But again, this critical hero image is blocked from rendering for 1.5 to 2.5 seconds.
Once again, the hero image is blocked from rendering because of the many scripts on the page, but Airbnb’s waterfall shows an interesting effect of the preloader. While the preloader, overall, makes pages load much quicker, in this case it actually hurts the user experience for Airbnb users. The Airbnb home page puts several scripts at the bottom of the page, but doesn’t load them asynchronously. While moving scripts to the bottom was a good performance optimization in 2007, that was before preloaders were created. Today, for optimal performance it’s important to load scripts asynchronously.
In this case, when Airbnb is loaded in a modern browser those scripts at the bottom get scheduled earlier by the preloader and end up being requests 3, 5, 6 and 9. They add up to 238K of gzipped JavaScript. Ungzipped it turns into 797K of JavaScript that has to be parsed and executed before the hero image can be displayed.
Image Custom Metrics
Most of the performance metrics used today focus on the mechanics of the browser (window.onload) or network performance (time to first byte and Resource Timing). Unfortunately, these don’t tell us enough about what matters the most: the user’s experience. When does the page’s content get shown to the user so she can start interacting with the page?
To measure what matters, we need to shift our focus to metrics that better represent the user experience. These metrics are specific to each individual website measuring the most important design elements on each page. Because they must be created on a case-by-case basis we call them custom metrics. The earliest and most well known example of a custom metric is in a 2012 article from Twitter where they describe how their most important performance metric is Time to First Tweet, defined as “the amount of time it takes from navigation (clicking the link) to viewing the first Tweet on each page’s timeline.” Note that they don’t talk about how long it takes to download the tweets. Instead, they care about when the tweet can be viewed.
Sites that have hero images need to do the same thing: focus on when the hero image is viewed. This is trickier than it sounds. There are no hooks in today’s browsers that can be used to know when content is viewable. But we can find a solution by thinking about the three things that block an image from rendering: synchronous scripts, stylesheets, and the image itself being slow.
Talking to some web performance experts (thanks Philip, Pat, Scott, Andy and Paul!), I identified five candidate techniques for measuring when an image is displayed:
- Resource Timing
- image onload handler
- mutation observer
- polling for offsetHeight
- inline script timer
I created a test page that has a synchronous script, a stylesheet, and an image that are programmed to take a specific amount of time to download (3 seconds, 5 seconds, and 1 second respectively). Running them in WebPagetest I confirmed that the image isn’t displayed until after 5 seconds. I then implemented each of the techniques and found that:
- Resource Timing reports a time of ~1.5 seconds, which is accurate for when the image downloads but is not accurate for measuring when the image is viewable.
- The image onload handler, mutation observer, and polling techniques all report a time of ~3 seconds which is too early.
- Only the inline script timer technique reports a time that matches when the image is displayed.
This test page addresses the scenarios of a synchronous script and stylesheet. We still need to find an accurate measurement technique for the case when the image itself is slow to load. A slight variation of the test page includes a 7-second image and, of the five techniques, only Resource Timing and image onload handler correctly measure when the image is displayed – the other techniques report a time that is too early. Of those two, image onload is preferred over Resource Timing because it’s simpler and more widely supported.
Therefore, to create a custom metric that determines when an image is displayed you should take the max of the values reported by the inline script timer and image onload.
We’re all pretty familiar with image onload handlers. The inline script timer is simple as well – it’s just an inline script that records the time immediately following the IMG tag. Here’s what the code looks like:
<img src="hero.jpg" onload="performance.mark('hero1')"> <script>performance.mark('hero2')</script>
The code above takes advantage of the User Timing API. It’s easy to polyfill for browsers that don’t support it; I recommend using Pat Meenan’s polyfill. You need to take the max value of the hero1 and hero2 marks; this can be done either on the client or on the backend server that’s collecting your metrics. Refer to my test page to see live code of all five measurement techniques.
The most important thing to do is get your hero images to render quickly. Use Custom Metrics to make sure you’re doing that well.
Steve Souders | 12-May-15 at 12:14 pm | Permalink |
In the old days, after being live for a morning there’d be several comments, questions, and corrections to a post like this. But so far, nothing.
I honestly was nervous about leaving so many unanswered questions in this post, but was looking forward to readers raising those questions. In the absence of that (!) I’ll ask the questions myself:
Any other unanswered questions? Any answers?
Tom | 12-May-15 at 1:11 pm | Permalink |
The real question is: why rendering timings are not available in the browser yet?
Jon | 12-May-15 at 2:43 pm | Permalink |
are there any emerging best practices for speeding up the loading of specific images in an HTML doc?
i feel like historically speaking, the web has been about balancing beautiful engaging content with speed and performance, and i find it hard to believe that with the move to using larger images/assets hasn’t been accompanied by performance enhancing techniques.
lazy-loading (particularly for GIFs) is one technique I’ve seen commonly used. any others out there?
Ash | 12-May-15 at 3:01 pm | Permalink |
As a n00b, I can’t comment much about what is going on, but I appreciate your introducing me to these tools.
That said, maybe I am just a cranky envious bastard, but the images all three sites are using are nothing but a turn off. I am not that pretty or wealthy and what those sites are telling me with those images is that their services are not for me.
Also, I hate hero images. I get it, it looks great on a tablet and phone, and probably in a board room, meeting room, and conference presentation, but it looks like a chore on my 24″ monitor. It mainly means I have to scroll down a hell of a lot to get to the real substance.
But honestly, I really did appreciate your walking through the tools and experiments.
Steve Souders | 12-May-15 at 5:08 pm | Permalink |
Tom: The W3C Web Performance Working Group has made great progress creating the Navigation Timing, Resource Timing, and User Timing specifications. But you’re right – we still need a way to measure things like JS execution, CSS styling, and rendering.
Steve Souders | 12-May-15 at 5:46 pm | Permalink |
Jon: For hero image website designs, I’m very excited about preload links. This would be a way to tell the browser to start downloading a critical resource (like a hero image) immediately. Ilya Grigorik mentions he hopes it’ll be in Chrome soon.
Until that’s supported, a follow-up tweet recommends the following:
But this test page shows that loading an image as a background for the HTML element, dynamically via JS, or as the first IMG tag in the page, first IMG tag is the fastest (at least in Chrome).
Yoav Weiss | 12-May-15 at 11:11 pm | Permalink |
The background image method is far from being the way to go.
Looking at the waterfall[1] for in latest stable Chrome, we can see that the background image’s download (“prefetch”) was initiated *last*, since background images have to wait for all external CSS to download and style to be calculated before a download can start (since a later CSS rule can modify the background, making the download useless).
We can also see that the dynamic JS added image (“dynamic”) is also one of the last ones to finish downloading in Chrome, most likely due to the fact that it’s not being displayed, and therefore has very low priority.
Like Ilya said, preload is required to properly handle these use-cases. I’m on it [2] :)
[1]: https://www.dropbox.com/s/bhk157upymj5h1q/img_download_waterfall.png?dl=0
[2]: https://codereview.chromium.org/1060863003/
Michael Scharnagl | 12-May-15 at 11:36 pm | Permalink |
I have been using the following method to show hero images as fast as possible but not 100% sure if this is the perfect technique right now.
First I load an img to make use of the preloader to load the image while using display: none do hide it visually.
Then in my CSS I use:
.header-img {
width: 100%;
height: 500px;
background-size: cover;
background-image: url(images/header-img.jpg);
}
to insert the same image as an background image. This gives me all the benefits of background images while still ensuring that the image gets shown pretty early.
More infos here: https://justmarkup.com/log/2015/02/02/prioritize-loading-of-background-images/
Using Preload links would solve this in a far better way, but until they are widely supported, what do you think about this technique?
Kornel | 13-May-15 at 2:50 am | Permalink |
This is the case why I can’t wait for HTTP/2 + progressive JPEG mashup!
An HTTP/2 server can send the first few KB of the hero JPEG first (which would serve as a low-resolution placeholder), then send all the styles, scripts, etc. and resume sending the rest of the hero image last.
This way you’d avoid the blank space without noticeably slowing anything else.
Michael Weber | 13-May-15 at 9:41 am | Permalink |
There are automatic ways to mitigate HID to some degree. We described this in the paper “ZOOMM: a parallel web browser engine for multicore mobile devices”: prefetch and process CSS in parallel (and then also prefetch resources from CSS).
Steve Souders | 13-May-15 at 9:52 am | Permalink |
Yoav: I agree that the IMG tag is the fastest way to get images downloaded. I ran my test page across Chrome, Firefox, IE 9-11, and Safari on WebPageTest. IMG was the best overall, followed by loading dynamically via JS. The CSS background image approach was the worst overall.
Michael S: I agree with your approach. It supports the conclusion that Yoav and I came to: the technique that gets images loading earliest is the simple IMG tag.
Kornel: Definitely HTTP/2 server-side push and LINK REL=PRELOAD will dramatically help this issue. I’m excited that Yoav is working on preload in Chrome!
hexalys | 13-May-15 at 3:55 pm | Permalink |
As a follow-up on my tweet on the CSS background technique. [1]
As Yoav said, Chromium/Blink altered the webkit preloader along the way so that it require a style-calc in order to trigger the download on the bg images.
As per my observation Safari 5.1 (perhaps earlier) and at least until Safari 7 did not have that requirement.
The reason I did not notice that is was still working for me even in recent Blink versions, is that in my use cases, I happen to trigger layout deliberately, as required by my JS feature script detection, by calling a document.clientWidth instantly after the inline style.
Like so:
html{ background: url(/hero.jpg) no-repeat -9999px -9999px; }
var ua={cw:html.clientWidth};
This wasn’t designed on my part to go against the preloader. It just happened to be a coincidence.
However for the Blink pre-loader to no longer consistently preload inline backgrounds like Safari does. I think this is mistake. At least not until “link ‘preload'” becomes a thing.
Google has been heavily promoting CSS inlining for the critical path. The strategy being that the above-the-fold should load and render ASAP. Which I don’t even necessarily agree with… But I agree that assets purposely placed first in the head by an author in the attempt of best performance SHOULD in fact all load early.
What would be the point of inlining CSS for this purpose as directed by Google’s best recommendation (which is also flat mandatory for the PageSpeed Insights score) if Blink is NOT going to request those assets ASAP. The preloader change is a step in the wrong direction for performance in my opinion. Safari’s previous approach in regards to inline css backgrounds did not need to be touched that way.
Even if ‘preload’ solve many of these cases. Which I obviously do favor overall. Let’s assume the author inline critical CSS on cold cache. The Blink engine force authors to duplicate the preload/load process with redundant link rel=preload markup in addition the css asset already there in the . That doesn’t any sense to me. Moreover, I now see Chrome 42 purposely “Stalling” the bg asset requests until it parse all the external CSS, even if that’s the only asset in the DOM. I am losing 50-200 ms in performance right there for CSS backgrounds for no good reason. AFAIK that is performance regression.
This is NOT what I expect the browser to do. The only valid case I’d expect that, is only if the inline CSS was within a media query.
[1] https://twitter.com/hexalys/status/598284530735124481
Scotty | 18-May-15 at 7:30 am | Permalink |
I’ve tried to optimize for this in the past, but it’s tough when dealing with a heavy CMS system such as Drupal. Having the images as embedded images and not a background is a huge improvement even if you lack some benefits of a css-background element.
Rene | 25-May-15 at 6:52 am | Permalink |
Super thanks for the article, we just implemented this with our new, hero image-heavy hotel booking site, vossy.com, by loading the index page background image as an image proper – the image is hidden but causes the browser to download the image before some other elements.
We have also added some code to try and pre-size the index page container to minimise the page jump.
The result is a way better than before, but most noticeably on index page refresh, which loads a new location hero image depending on what location is being searched.
Roman Fuchs | 30-May-15 at 5:27 pm | Permalink |
Hi Steve! Interesting article and great to meet you again at Velocity!
I looked again a bit in how we load the hero image and the scripts at Airbnb, and I don’t think I fully agree.
While the preloader will load these scripts early and these bytes are of course competing with possibly more important resources, I disagree with “797K of JavaScript has to be parsed and executed before the hero image can be displayed.” I don’t think the image is being blocked by the JavaScript. It can certainly be rendered before the JS is being parsed or executed. It’s easy to find a counter-example, e.g. [1]
The reason why there is such a large delay until the hero image is rendered is that we’re currently running a header experiment (twice the markup!), so a lot of markup needs to be rendered and some inline JavaScript needs to be executed before the hero image gets rendered.
For instance in [2] there is also a delay. But in the timeline [3] you can see, what is delaying the rendering of the image:
Finish Loading sanfrancisco.jpg
[..]
Finish Loading common.css
Parse common.css
Evaluate inline script
Parse HTML (20m), including header_cookie.js and inline script for header experiment
Recalculate Style (20ms)
Layout (Layout tree size 801 nodes, 224ms)
Paint <– hero image is rendered
[..]
Evaluate libs_jquery_2x.js (163ms)
Other JS bundles..
[..]
Unless I want to implement the combination of DOM loading with "async=false" and using "readyState" for IE<10 and possibly adding "” as suggested by Jake Archibald here: [4], I don’t think there’s anything wrong with adding the scripts at the bottom and let the preloaders do their job. I’d go for simplicity here.
[1]: http://www.webpagetest.org/video/compare.php?tests=150529_70_9E-r%3A1-c%3A0&thumbSize=200&ival=100&end=visual
[2]: http://www.webpagetest.org/result/150529_Y0_19KV/#run3
[3]: http://cdn.webpagetest.org/chrome/inspector-20150519/inspector.html?loadTimelineFromURL=/getTimeline.php?test=150529_Y0_19KV&run=3&cached=0
[4]: http://www.html5rocks.com/en/tutorials/speed/script-loading/
Roman Fuchs | 30-May-15 at 5:31 pm | Permalink |
meant to say “a lot of markup needs to be parsed and layout calculated” [re: header experiment]
Steve Souders | 31-May-15 at 1:50 pm | Permalink |
Hi, Roman.
Cool, thanks for clarifying what’s going on in the Airbnb case. In your case it might be that synchronous scripts at the bottom is not an issue, but in general it should be avoided. Here’s why:
When the scripts at the bottom get downloaded, they’re parsed and executed. If it’s a lot of JavaScript (like 800K) then that parse & execution could take a significant amount of time. If the image gets downloaded BEFORE those scripts at the bottom arrive, that’ll work find and the image gets rendered. BUT, if the scripts get downloaded before the image (which is likely since the preloader gives them a higher priority) then the parse & execution of those scripts at the bottom WILL block the image from rendering. Here’s an example that shows this behavior: the quick image is blocked from rendering for 4 seconds because of a script at the bottom that takes 4 seconds to execute:
https://stevesouders.com/cuzillion/?c0=hj1hfff2_0_f&c1=bi1hfff1_0_f&c2=bj1hfff1_4_f
The other problem is the limited pool of TCP connections. If you have a total of more than six scripts at the top & bottom (which is easy to do), then the image (which is a lower priority) has to fight to get a TCP connection from the scripts. The prioritization and connection allocation algorithms differ across browsers, but it’s likely that the hero image will get downloaded later than desired, esp. if it’s not the first image in the page. Here’s a test page for this scenario:
https://stevesouders.com/cuzillion/?c0=hj1hfff2_0_f&c1=bi1hfff2_0_f&c2=bj1hfff2_0_f&c3=bj1hfff2_0_f&c4=bj1hfff2_0_f&c5=bj1hfff2_0_f&c6=bj1hfff2_0_f&c7=bj1hfff2_0_f&c8=bj1hfff2_0_f&t=1433104949
For these reasons, render blocked from execution and TCP connection contention, it’s better to make scripts ASYNC, even if they’re at the bottom of the page.
Roman Fuchs | 31-May-15 at 7:01 pm | Permalink |
Makes sense. Yeah, thanks for the examples.
Ved | 02-Jun-15 at 3:33 pm | Permalink |
Great article and great discussion. So From your first reply, are you suggesting that to not use HERO images as background img and instead use them as tag ?
Also what about the fonts ? I am assuming if we are using external fonts than they also needs to be taken into consideration of how much time it takes to load and render them on screen.
Steve Souders | 02-Jun-15 at 3:42 pm | Permalink |
Ved: Yes, images appear more quickly if they’re inserted using the IMG tag than as a CSS background image. Fonts are important but don’t block images.
Joe G | 17-Jun-15 at 5:32 pm | Permalink |
Great article on a very important topic. One thing to clarify – current performance tools using a film strip view doesn’t really measure when they user sees the image? I guess I had always assumed it did.
A few more things to consider in hero approaches. Some sites use just an image, while others use a carousel. It would seem the carousel approach would increase download due to javascript. Interesting approach by amazon loads the hero image first, then the carousel loads.
Eric | 02-Jul-15 at 11:43 am | Permalink |
Hi Steve,
You said, “The CSS background image approach was the worst overall.”, but what about a CSS background image inline? Is it still the worst? I sometimes use this technique to create responsive hero images with media query.
Steve Souders | 06-Jul-15 at 5:24 pm | Permalink |
Eric: Inlining images as base 64 encoded images is only good for small images – not hero images.
Manu | 29-Jul-15 at 2:31 pm | Permalink |
@Steve Sauders
I’m still confused on the answer to the first question,
1. Great – I highlighted a bunch of cases where hero images are slow to render, and described ways to measure that in live user traffic. But I didn’t talk about how to fix the actual problem! What’s the fix to make hero images load more quickly?
Is there a simple fix to the above problem?
Currently i’m using inline style on div to place the background hero image. Is it bad? Would it better to pre-load images using tags instead?
Steve Souders | 03-Aug-15 at 1:44 pm | Permalink |
Manu: Using the IMG tag would be faster because it’ll be found by the preloader. Using CSS (even an inline style attribute) requires that the HTML parser create the DOM element and apply the styling, but the HTML parser can be blocked.