Tags: navigation

5

sparkline

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.

Sticky headers

I made a little tweak to The Session today. The navigation bar across the top is “sticky” now—it doesn’t scroll with the rest of the content.

I made sure that the stickiness only kicks in if the screen is both wide and tall enough to warrant it. Vertical media queries are your friend!

But it’s not enough to just put some position: fixed CSS inside a media query. There are some knock-on effects that I needed to mitigate.

I use the space bar to paginate through long pages. It drives me nuts when sites with sticky headers don’t accommodate this. I made use of Tim Murtaugh’s sticky pagination fixer. It makes sure that page-jumping with the keyboard (using the space bar or page down) still works. I remember when I linked to this script two years ago, thinking “I bet this will come in handy one day.” Past me was right!

The other “gotcha!” with having a sticky header is making sure that in-page anchors still work. Nicolas Gallagher covers the options for this in a post called Jump links and viewport positioning. Here’s the CSS I ended up using:

:target:before {
    content: '';
    display: block;
    height: 3em;
    margin: -3em 0 0;
}

I also needed to check any of my existing JavaScript to see if I was using scrollTo anywhere, and adjust the calculations to account for the newly-sticky header.

Anyway, just a few things to consider if you’re going to make a navigational element “sticky”:

  1. Use min-height in your media query,
  2. Take care of keyboard-initiated page scrolling,
  3. Adjust the positioning of in-page links.

Progresponsive

Brad has done a great job in documenting navigation patterns for responsive designs. More recently I came across Erick Arbé’s similar collection of patterns for responsive navigation. And, of course, at the Responsive Day Out, David gave a presentation on the subject.

David Bushell: Responsive Navigation on Huffduffer

As I mentioned in the chat after David’s talk, choosing a pattern doesn’t need to be an either/or decision. You can start with a simple solution and progressively enhance to a more complex navigation pattern.

Take the footer-anchor pattern, for example. I really, really like this pattern. It doesn’t require any JavaScript whatsoever; just a simple hyperlink from the top of the page that links to the fragment identifier of the navigation at the bottom of the page. It works on just about every device.

But you don’t have to stop there. Now that you’ve got a simple solution that works everywhere, you can enhance it for more capable browsers.

Take a look at this example that applies the off-canvas pattern for browsers capable of handling the JavaScript and CSS required.

You can see the two patterns in action by looking at the source in JS Bin. If you toggle the “Auto-run JS” checkbox, you can see both behaviours. Without JavaScript you get the footer-anchor pattern. With JavaScript (and a capable browser) you get the off-canvas pattern.

I haven’t applied any media queries in this instance, but it would be pretty straightforward to apply absolute positioning or the display: table hack to display the navigation by default at wider screen sizes. I’ll leave that as an exercise for the reader (bonus points: apply the off-canvas from the right of the viewport rather than the left).

Feel free to peruse the somewhat simplistic code. I’m doing a bit of feature detection—or cutting the mustard—to test for querySelector and addEventListener. If a browser passes the test, a class is applied to the document root and some JavaScript is executed on page load to toggle the off-canvas behaviour.

On a recent project, I found myself implementing a number of different navigation patterns: off-canvas, overlay, and progressive disclosure. But each one began as an instance of the simple footer-anchor pattern.

Progressive enhancement, baby. Still not dead, still important.

Navicon

Daniel recently asked a question on Twitter:

It was this article by Malarkey that he was looking for. Andy did a great job of comparing the iconography used for navigation in mobile apps and responsive sites. His conclusion:

Unless our navigation’s arranged in a grid (and so we should use a grid icon), I’m putting my weight behind three lines because I think it’s most recognisable as navigation to the average person.

The three-lines icon is certainly very popular, as can be seen in this collection of mobile navigation icons I gathered together on Dribbble.

But Tom has some reservations:

Andy Davies points out another potential issue:

I noticed this in the more recent versions of Android too. It does indeed look a little odd to see the same icon used in the browser chrome and in the document within the browser.

Double navigation (BUT WHAT DOES IT MEAN!?!?)

But I still think it’s a good shorthand for revealing a list of items.

The unicode character ☰ ☰ (U+2630) is the Chinese trigram for sky (or heaven)—one of the eight bagua. It consists of three horizontal lines. Now that could be a handy resolution-independent way of representing navigation.

Dribbble — Mobile First

Alas, when I tested this on a range of mobile devices, some of them just showed the square box of unicode disappointment. I had much better luck with the unicode symbol for black down-pointing triangle▼ (U+25BC).

Dribbble — Navigation link

Mind you, with a combination of @font-face and sub-setting we’re not limited to what the browser ships with—we can provide our own icons in a font file, like what Pictos is doing.

Re-tabulate

Right after I wrote about combining flexbox with responsive design—to switch the display of content and navigation based on browser size—I received an email from Raphaël Goetter. He pointed out a really elegant solution to the same use-case that makes use of display:table.

Let’s take the same markup as before:

<body>
<div role="main">
<p>This is the main content.</p>
</div>
<nav role="navigation">
<p>This is the navigation.</p>
<ol>
<li><a href="#">foo</a></li>
<li><a href="#">bar</a></li>
<li><a href="#">baz</a></li>
</ol>
</nav>
</body>

The source order reflects the order I want on small-screen devices (feature phones, smart phones, etc.). Once the viewport allows it, I’d like to put that navigation at the top. I can do this by wrapping some display declarations in a media query:

@media screen and (min-width: 30em) {
    body {
        display: table;
        caption-side: top;
    }
    [role="navigation"] {
        display: table-caption;
    }
}

That’s it. It works much like box-orient:vertical with box-direction:reverse but because this is good ol’ CSS 2.1, it’s very well supported.

We can solve the other issue too: making those list items display horizontally on larger screens:

[role="navigation"] ol {
    display: table-row;
}
[role="navigation"] ol li {
    display: table-cell;
}

Once again, I’ve put a gist up on Github (get me! I’m like a proper computer nerd).

Update: And Remy has put it on JSbin so you can see it in action (resize the live preview pane).

So there you go: we’ve at least two different mechanisms in CSS to re-order the display of content and navigation in response to screen real-estate. The default is content first, navigation second—a pattern that Luke talked about in this interview with Jared:

Yeah, one of the design principles that I’ll be talking on the tour about, for mobile, is content first, navigation second; which is just really putting something up right away that somebody can engage with, and saving the pivoting and the navigating for later.

There’s, basically, UI patterns that you can use to make that happen. I’m still surprised at how many, both mobile websites and applications, the first thing they give you is a menu of choices, instead of content.

Don’t get me wrong, the menu’s important, and you can get to it, but it’s actually the content that the immediacy of mobile, and the fact that you’re probably on a slower network, and in some cases you’re even paying for your data transfers, right? Giving you a list of choices as your first time experience tends not to work so well.

Luke Wroblewski — Designing Mobile Web Experiences » UIE Brain Sparks on Huffduffer