How To Improve Largest Contentful Paint for Faster Load Times - Calibre
A no-nonsense checklist of good performance advice from Karolina.
A no-nonsense checklist of good performance advice from Karolina.
I think I’ve found some more strange service worker behaviour in Chrome.
It all started when I was checking out the very nice new redesign of WebPageTest. I figured while I was there, I’d run some of my sites through it. I passed in a URL from The Session. When the test finished, I noticed that the “screenshot” tab said that something was being logged to the console. That’s odd! And the file doing the logging was the service worker script.
I fired up Chrome (which isn’t my usual browser), and started navigating around The Session with dev tools open to see what appeared in the console. Sure enough, there was a failed fetch
attempt being logged. The only time my service worker script logs anything is in the catch
clause of fetching pages from the network. So Chrome was trying to fetch a web page, failing, and logging this error:
The service worker navigation preload request failed with a network error.
But all my pages were loading just fine. So where was the error coming from?
After a lot of spelunking and debugging, I think I’ve figured out what’s happening…
First of all, I’m making use of navigation preloads in my service worker. That’s all fine.
Secondly, the website is a progressive web app. It has a manifest file that specifies some metadata, including start_url
. If someone adds the site to their home screen, this is the URL that will open.
Thirdly, Google recently announced that they’re tightening up the criteria for displaying install prompts for progressive web apps. If there’s no network connection, the site still needs to return a 200 OK
response: either a cached copy of the URL or a custom offline page.
So here’s what I think is happening. When I navigate to a page on the site in Chrome, the service worker handles the navigation just fine. It also parses the manifest file I’ve linked to and checks to see if that start URL would load if there were no network connection. And that’s when the error gets logged.
I only noticed this behaviour because I had specified a query string on my start URL in the manifest file. Instead of a start_url
value of /
, I’ve set a start_url
value of /?homescreen
. And when the error shows up in the console, the URL being fetched is /?homescreen
.
Crucially, I’m not seeing a warning in the console saying “Site cannot be installed: Page does not work offline.” So I think this is all fine. If I were actually offline, there would indeed be an error logged to the console and that start_url
request would respond with my custom offline page. It’s just a bit confusing that the error is being logged when I’m online.
I thought I’d share this just in case anyone else is logging errors to the console in the catch
clause of fetches and is seeing an error even when everything appears to be working fine. I think there’s nothing to worry about.
Update: Jake confirmed my diagnosis and agreed that the error is a bit confusing. The good news is that it’s changing. In Chrome Canary the error message has already been updated to:
DOMException: The service worker navigation preload request failed due to a network error. This may have been an actual network error, or caused by the browser simulating offline to see if the page works offline: see https://w3c.github.io/manifest/#installability-signals
Much better!
Did you know there’s an imagesrcset
attribute you can put on link rel="preload" as="image"
(along with an imagesizes
attribute)?
I didn’t. (Until Amber pointed this out.)
There’s a feature in service workers called navigation preloads. It’s relatively recent, so it isn’t supported in every browser, but it’s still well worth using.
Here’s the problem it solves…
If someone makes a return visit to your site, and the service worker you installed on their machine isn’t active yet, the service worker boots up, and then executes its instructions. If those instructions say “fetch the page from the network”, then you’re basically telling the browser to do what it would’ve done anyway if there were no service worker installed. The only difference is that there’s been a slight delay because the service worker had to boot up first.
It’s not a massive performance hit, but it’s still a bit annoying. It would be better if the service worker could boot up and still be requesting the page at the same time, like it would do if no service worker were present. That’s where navigation preloads come in.
Navigation preloads—like the name suggests—are only initiated when someone navigates to a URL on your site, either by following a link, or a bookmark, or by typing a URL directly into a browser. Navigation preloads don’t apply to requests made by a web page for things like images, style sheets, and scripts. By the time a request is made for one of those, the service worker is already up and running.
To enable navigation preloads, call the enable()
method on registration.navigationPreload
during the activate
event in your service worker script. But first do a little feature detection to make sure registration.navigationPreload
exists in this browser:
if (registration.navigationPreload) {
addEventListener('activate', activateEvent => {
activateEvent.waitUntil(
registration.navigationPreload.enable()
);
});
}
If you’ve already got event listeners on the activate
event, that’s absolutely fine: addEventListener
isn’t exclusive—you can use it to assign multiple tasks to the same event.
Now you need to make use of navigation preloads when you’re responding to fetch
events. So if your strategy is to look in the cache first, there’s probably no point enabling navigation preloads. But if your default strategy is to fetch a page from the network, this will help.
Let’s say your current strategy for handling page requests looks like this:
addEventListener('fetch', fetchEvent => {
const request = fetchEvent.request;
if (request.headers.get('Accept').includes('text/html')) {
fetchEvent.respondWith(
fetch(request)
.then( responseFromFetch => {
// maybe cache this response for later here.
return responseFromFetch;
})
.catch( fetchError => {
return caches.match(request)
.then( responseFromCache => {
return responseFromCache || caches.match('/offline');
});
})
);
}
});
That’s a fairly standard strategy: try the network first; if that doesn’t work, try the cache; as a last resort, show an offline page.
It’s that first step (“try the network first”) that can benefit from navigation preloads. If a preload request is already in flight, you’ll want to use that instead of firing off a new fetch
request. Otherwise you’re making two requests for the same file.
To find out if a preload request is underway, you can check for the existence of the preloadResponse
promise, which will be made available as a property of the fetch event you’re handling:
fetchEvent.preloadResponse
If that exists, you’ll want to use it instead of fetch(request)
.
if (fetchEvent.preloadResponse) {
// do something with fetchEvent.preloadResponse
} else {
// do something with fetch(request)
}
You could structure your code like this:
addEventListener('fetch', fetchEvent => {
const request = fetchEvent.request;
if (request.headers.get('Accept').includes('text/html')) {
if (fetchEvent.preloadResponse) {
fetchEvent.respondWith(
fetchEvent.preloadResponse
.then( responseFromPreload => {
// maybe cache this response for later here.
return responseFromPreload;
})
.catch( preloadError => {
return caches.match(request)
.then( responseFromCache => {
return responseFromCache || caches.match('/offline');
});
})
);
} else {
fetchEvent.respondWith(
fetch(request)
.then( responseFromFetch => {
// maybe cache this response for later here.
return responseFromFetch;
})
.catch( fetchError => {
return caches.match(request)
.then( responseFromCache => {
return responseFromCache || caches.match('/offline');
});
})
);
}
}
});
But that’s not very DRY. Your logic is identical, regardless of whether the response is coming from fetch(request)
or from fetchEvent.preloadResponse
. It would be better if you could minimise the amount of duplication.
One way of doing that is to abstract away the promise you’re going to use into a variable. Let’s call it retrieve
. If a preload is underway, we’ll assign it to that variable:
let retrieve;
if (fetchEvent.preloadResponse) {
retrieve = fetchEvent.preloadResponse;
}
If there is no preload happening (or this browser doesn’t support it), assign a regular fetch request to the retrieve
variable:
let retrieve;
if (fetchEvent.preloadResponse) {
retrieve = fetchEvent.preloadResponse;
} else {
retrieve = fetch(request);
}
If you like, you can squash that into a ternary operator:
const retrieve = fetchEvent.preloadResponse ? fetchEvent.preloadResponse : fetch(request);
Use whichever syntax you find more readable.
Now you can apply the same logic, regardless of whether retrieve
is a preload navigation or a fetch request:
addEventListener('fetch', fetchEvent => {
const request = fetchEvent.request;
if (request.headers.get('Accept').includes('text/html')) {
const retrieve = fetchEvent.preloadResponse ? fetchEvent.preloadResponse : fetch(request);
fetchEvent.respondWith(
retrieve
.then( responseFromRetrieve => {
// maybe cache this response for later here.
return responseFromRetrieve;
})
.catch( fetchError => {
return caches.match(request)
.then( responseFromCache => {
return responseFromCache || caches.match('/offline');
});
})
);
}
});
I think that’s the least invasive way to update your existing service worker script to take advantage of navigation preloads.
Like I said, preload navigations can give a bit of a performance boost if you’re using a network-first strategy. That’s what I’m doing here on adactio.com and on thesession.org so I’ve updated their service workers to take advantage of navigation preloads. But on Resilient Web Design, which uses a cache-first strategy, there wouldn’t be much point enabling navigation preloads.
Jeff Posnick made this point in his write-up of bringing service workers to Google search:
Adding a service worker to your web app means inserting an additional piece of JavaScript that needs to be loaded and executed before your web app gets responses to its requests. If those responses end up coming from a local cache rather than from the network, then the overhead of running the service worker is usually negligible in comparison to the performance win from going cache-first. But if you know that your service worker always has to consult the network when handling navigation requests, using navigation preload is a crucial performance win.
Oh, and those browsers that don’t yet support navigation preloads? No problem. It’s a progressive enhancement. Everything still works just like it did before. And having a service worker on your site in the first place is itself a progressive enhancement. So enabling navigation preloads is like a progressive enhancement within a progressive enhancement. It’s progressive enhancements all the way down!
By the way, if all of this service worker stuff sounds like gibberish, but you wish you understood it, I think my book, Going Offline, will prove quite valuable.
Scott re-examines the browser support for loading everything-but-the-critical-CSS asynchronously and finds that it might now be as straightforward as this one declaration:
<link rel="stylesheet" href="/https/adactio.com/path/to/my.css" media="print" onload="this.media='all'">
I love the fact the Filament Group are actively looking at how deprecate their loadCSS
polyfill—exactly the right attitude for polyfills in general.
Following on from Harry’s slides, here’s another round-up of thoserel
attribute values that begin with pre
.
Slides from Harry’s deep dive into rel
values: preconnect
, prefetch
, and preload
.
A good roundup of techniques for responsible prefetching from Katie Hempenius.
This checklist came in very handy during a performance-related workshop I was running today (I may have said the sentence “Always ask yourself What Would Zach Do?”).
- Start Important Font Downloads Earlier (Start a Web Font load)
- Prioritize Readable Text (Behavior while a Web Font is loading)
- Make Fonts Smaller (Reduce Web Font load time)
- Reduce Movement during Page Load (Behavior after a Web Font has loaded)
The first two are really straightforward to implement (with rel="preload"
and font-display
). The second two take more work (with subsetting and the font loading API).
AMP pages aren’t fast because of the AMP format. AMP pages are fast when you visit one via Google search …because of Google’s monopoly on preloading:
Technically, a clever trick. It’s hard to argue with that. Yet I consider it cheating and anti competitive behavior.
Preloading is exclusive to AMP. Google does not preload non-AMP pages. If Google would have a genuine interest in speeding up the whole web on mobile, it could simply preload resources of non-AMP pages as well. Not doing this is a strong hint that another agenda is at work, to say the least.
Ooh, this is clever! Scott shows how you can use rel="preload"
to conditionally load JavaScript (say, for screens above a certain size). The browser support isn’t quite there yet, but the thinking here is smart.
A comprehensive overview of rel="preload"
which looks very useful indeed …I just wish it wasn’t (like “nofollow”) a nonsensical value for the rel
attribute: a resource can’t have a relationship of “preload” to the linking document.
A collection of Flash preloaders. Out of context, they make for surprisingly compelling viewing all together.