Tags: navigation

54

sparkline

Thursday, August 1st, 2019

Navigation preloads in service workers

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.

  1. The service worker activates.
  2. The service worker fetches the file.
  3. The service worker does something with the response.

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.

  1. The service worker activates while simultaneously requesting the file.
  2. The service worker does something with the response.

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.

Sunday, June 30th, 2019

Lights at sea

Lighthouses of the world, mapped.

Wednesday, June 19th, 2019

Using Hamburger Menus? Try Sausage Links · Bradley Taunt

Another take on the scrolling navigation pattern. However you feel about the implementation details, it’s got to better than the “teenage tidying” method of shoving everything behind a hamburger icon.

Monday, June 17th, 2019

First You Make the Maps

How cartography made early modern global trade possible.

Maps and legends. Beautiful!

Friday, March 22nd, 2019

NASA’s Visual Universe

An interesting way of navigating through a massive amount of archival imagery from NASA.

Tuesday, January 1st, 2019

The Elements of UI Engineering - Overreacted

These are good challenges to think about. Almost all of them are user-focused, and there’s a refreshing focus away from reaching for a library:

It’s tempting to read about these problems with a particular view library or a data fetching library in mind as a solution. But I encourage you to pretend that these libraries don’t exist, and read again from that perspective. How would you approach solving these issues?

Tuesday, July 31st, 2018

Refresh – A fresh approach to the browser

Some interesting ideas for evolving the web browser. I’m very interested in the ideas about navigating our browser history—that feels like a very underappreciated goldmine with a direct lineage to the “associative trails” imagined for the memex.

Monday, May 21st, 2018

Meet swup

This looks like a handy library for managing page transitions on sites that are not single page apps.

Here’s the code.

I’ve said it before and I’ll say it again, but I really think that this handles 80% of the justification for using a single page app architecture.

Monday, April 23rd, 2018

page-transitions-travelapp

A demo of page transition animations by Sarah—she’s written about how she did it. I really like it as an example of progressive enhancement: you can navigate around the site just fine, but with JavaScript you get the smooth transitions as a bonus.

All of this reminds me of Jake’s proposal for navigation transitions in the browser. I honestly think this would solve 80% of the use-cases for single page apps.

Friday, March 30th, 2018

Focusing on Focus Styles | CSS-Tricks

A deep dive into the :focus pseudo-class and why it’s important.

Sunday, January 28th, 2018

A Tale of Two Rooms: Understanding screen reader navigation | The Paciello Group

A nice analogy to help explain what it’s like to navigate with a screen reader—and how much well-structured markup can help make it easier.

Friday, January 5th, 2018

Improving the Accessibility of 24 ways | CSS-Tricks

Paul walks us through the process of making some incremental accessibility improvements to this year’s 24 Ways.

Creating something new will always attract attention and admiration, but there’s an under-celebrated nobility in improving what already exists. While not all changes may be visual, they can have just as much impact.

Thursday, October 5th, 2017

Creating accessible menus-Part 1

James has been tweaking the accessibility of his site navigation. I’m looking forward to the sequel.

Monday, September 25th, 2017

jakearchibald/navigation-transitions

I honestly think if browsers implemented this, 80% of client-rendered Single Page Apps could be done as regular good ol’-fashioned websites.

Having to reimplement navigation for a simple transition is a bit much, often leading developers to use large frameworks where they could otherwise be avoided. This proposal provides a low-level way to create transitions while maintaining regular browser navigation.

Friday, September 1st, 2017

Progressively Worse Apps

This article makes a good point about client-rendered pages:

Asynchronously loaded page elements shift click targets, resulting in a usability nightmare.

…but this has nothing, absolutely nothing to do with progressive web apps.

More fuel for the fire of evidence that far too many people think that progressive web apps and single page apps are one and the same.

Sunday, July 2nd, 2017

Sticky headers

A three-part series by Remy looking at one interface pattern (a sticky header) and how his code evolved and changed:

  1. Sticky headers
  2. Smooth scroll & sticky navigation
  3. CSS sticky nav & smooth scroll

Sunday, January 15th, 2017

Using the aria-current attribute – Tink

The aria-current attribute is very handy and easy to implement. Léonie explains it really well here.

Monday, September 12th, 2016

CSS Mega Dropdown | CodyHouse

I’m no fan of mega menus, and if a site were being designed from scratch, I’d do everything I could to avoid them, but on some existing projects they’re an unavoidable necessity (the design equivalent of technical debt). In those situations, this looks like a really nice, responsive approach.

Sunday, August 7th, 2016

The Future of Browser History — Free Code Camp

I’ve been thinking about this a lot lately. It feels like a user’s browser history is an incredibly rich seam of valuable information just waiting to be presented in a more interesting way.

Wednesday, June 29th, 2016

Completely CSS: Progressively Collapsing Navigation | Kenan Yusuf

One way of implementing the growing/shrinking navigation pattern—an alternative to just shoving everything behind a hamburger icon.