Cloud Four Blog

Technical notes, War stories and anecdotes

Service Workers at Scale, Part II: Handling Fallback Resources

Part I of this series established that some of the challenges in building service workers for complex websites stem from having to accommodate page request variations. In this iteration, we’ll touch on how to handle similar variations in the acquiring and delivering of fallback resources.

Pre-caching fallback dependencies

A familiar pattern for caching dependencies is to request them in bulk during the installation stage. We began with this approach in our own service worker, specifying path strings for all fallback resources:

addEventListener('install', event => {
  event.waitUntil(
    caches.open('dependencies')
      .then(cache => {
        return cache.addAll([
          '/offline',
          '/books-offline',
          '/assets/offline-avatar.png',
          '/assets/offline-photo.png'
        ]);
      )
      .catch(err => console.warn(err))
      .then(skipWaiting())
  );
});

Cache.addAll() converts each element of the array it receives into a new Request object, so you can conveniently pass it an array of strings. This simple approach worked well during local development, but there were some issues on our more complicated test server. We needed more control over the requests made by Cache.addAll(), so we created them explicitly:

cache.addAll([
  new Request('/offline', {...}),
  new Request('/books-offline', {...}),
  new Request('/assets/offline-avatar.png', {...}),
  new Request('/assets/offline-photo.png', {...})
]);

Constructing our own requests gave us the ability to override their default options. The necessity to supply these options became obvious once we saw 401 responses to our fallback page requests. The server had enabled HTTP auth, so requests sent from our worker in attempt to pre-cache resources were failing.

The credentials option solved this problem, allowing our requests to break through the authentication barrier:

cache.addAll([
  new Request('/offline', {credentials: 'same-origin'}),
  // ...
]);

We also decided to use the cache option for future-proofing. This will be useful for controlling how requests interact with the HTTP cache. While it currently only works in Firefox Developer Edition, we included it to make sure pre-cached responses are fresh1:

cache.addAll([
  new Request('/assets/offline-photo.png', {cache: 'reload'}),
  // ...
]);

For an overview of other cache option values, check out Jake Archibald’s article with various examples of service worker and HTTP cache interoperability.

Responding with fallback images

Our pre-cache items include generic images intended to serve as “fallbacks” for unfulfilled requests. We needed to account for two different fallback image types, each with their own visual treatment:

  1. Images embedded in articles
  2. Avatars for authors and commenters

To determine which fallback should be used for a given request, we associated each with a URL hostname:

const fallbackImages = new Map([
  [location.hostname, '/assets/offline-photo.png'],
  ['secure.gravatar.com', '/assets/offline-avatar.png']
]);

Using a map like this, we can conveniently lookup the proper fallback based on the URL of an unfulfilled request:

function matchFallback (req) {
  const {hostname} = new URL(req.url);
 
  if (isImageRequest(req)) {
    const image = fallbackImages.get(hostname);
    return caches.match(image);
  }
 
  // ...
}

“Redirecting” to offline pages

As with our fallback images, we also needed to accommodate a bit of variation in our handling of fallback pages. Some pages that we wanted to make available offline had too many images to justify pre-caching. In these cases, simplified versions of those pages (minus the images) were created to use as substitutes, as if they were redirected.

Because all of the pages with offline variations are local, they can be mapped by their URL pathname, and incorporated into our matchFallback() handler accordingly:

const fallbackPages = new Map([
  ['/books', '/books-offline'],
  ['/workshops', '/workshops-offline']
]);
function matchFallback (req) {
  const {hostname, pathname} = new URL(req.url);
 
  if (isImageRequest(req)) {
    const imagePath = fallbackImages.get(hostname);
    return caches.match(imagePath);
  }
 
  if (isPageRequest(req)) {
    const pagePath = fallbackPages.get(pathname);
    return caches.match(pagePath);
  }
 
  // Use an new response if nothing better can be found.
  return Promise.resolve(new Response(/* ... */));
}

Coming up next: Cache trimming and invalidation

In the next part of this series, we’ll cover strategies for invalidating old caches and limiting the amount of storage space they can occupy.


  1. To fill in for the sparse browser implementation, it’s recommended to use some form of cache-busting when pre-caching static resources. 

Android Instant Apps, Progressive Web Apps and the Future of the Web

Google’s announcement of Android Instant Apps caused me to fear for the future of the web for the first time.

I’ve participated in many native versus web debates over the years. We even contemplated becoming an iOS-only shop after working on the Obama ’08 iPhone App.

But we chose to bet on the future of the web, and I had never doubted that decision in the years since until seeing Android Instant Apps.

Why Android Instant Apps Unnerved Me

When we placed that bet on the web years ago, we didn’t bet on the web “winning”. I didn’t think there was a contest to win. I expected apps and web to live side-by-side, and thus far, it has played out that way.

Plus, there were three factors that made me believe the web was the correct long term bet:

  1. PhoneGap/Cordova showed how most apps could be built using web technology once the web got access to device capabilities and features like push notifications.
  2. Scott Jenson’s Mobile Apps Must Die and the Triumph of the Mundane made compelling arguments that the app store model wouldn’t scale into an Internet of Things future. This belief is the foundation of the Physical Web project.
  3. Worst-case scenario, it seemed like a good future could be made simply building websites to promote apps.

We’ve finally reached a point where the promise of PhoneGap is becoming reality due to browser advances and Progressive Web Apps. And yet just as that promise is about to bear fruit, Android Instant Apps come along.

Stephanie Rieger once observed that we often install apps; give them space on our devices; and use our bandwidth to update them all “just in case” we need them. What we really need is “just in time” apps instead of “just in case” apps.

We need to use apps and throw them away. These sorts of transient experiences are something the web is excellent and that apps are poor at providing.

That is until Android Instant Apps which, in theory, make just-in-time apps possible.

And it is notable that one of the examples Google is showing off not only uses a common Physical Web example—a parking meter, but also uses the Physical Web beacon to launch the Android Instant App without ever opening the browser.

And if search results can open apps without needing to install apps, well, there goes my future career building websites for those apps.

Android Instant Apps may be vaporware

This year’s I/O keynote was full of promises and short on shipping code. Android Instant Apps was a prime example.

The scuttlebutt at the event is that Android Instant Apps have a long ways to go before they are ready for prime time. There are big questions about security, privacy, performance, and user experience that remain to be solved. And converting existing apps to Android Instant Apps in a single day is…optimistic.

But even if there are challenges, Android Instant Apps are a shot across the bow of the web. If there was ever any doubt that the owners of native platforms would eliminate the web if they could, there should be no doubt now.

Progressive Web Apps

At I/O, Google also heavily promoted the best alternative that we’ve had yet to native apps: Progressive Web Apps. The fact they were both promoted by the same company at the same event still blows my mind.

Over the last few days, there has been a lot of debate about progressive web apps between people who appear to be in violent agreement with each other. There’s been a lot more discussion than I can cover here so I am going to try (and probably fail) to summarize the debate thusly:

  1. Early progressive web apps are not responsive, not progressive, and only work on one platform and thus Google shouldn’t be promoting them.
  2. Chrome’s criteria for the app install prompt doesn’t support URLs as well as it should.
  3. Progressive web apps are a Google thing that they’re pushing that only works on Android to the determent of the broader web.
  4. People spend far too much time talking about apps and trying to make things that compete with native.

On the first two points, there is mostly broad consensus. The folks on the Chrome team dislike that the best examples they have thus far all have shortcomings, but they are the best ones have so far. They want to encourage developers to make them better instead of chastising them for not doing everything right out of the gate.

I sympathize with both perspectives. I agree with Michael Scharnagl when he cautions us to not repeat the errors from the beginning of responsive web design when it comes to progressive web apps.

And yet, if a client came to us and said they wanted to move to responsive web design, but they felt it was risky, I might very well suggest they start by using device detection to route mobile traffic to a m-dot site where they could test the design before rolling it out further.

That is what the BBC did with their beta responsive web design and they were praised for their work. How is that substantially different than Flipkart’s Progressive Web App for which they are getting grief? Yes, it started on Android Chrome only, but they promised to make it work elsewhere and are about to deliver on those promises.

I think the only major difference is that by the time the BBC did their mobile-only beta, we already had the Boston Globe to look at. Plus, we didn’t have a large company like Google using the BBC site as an example.

Keep in mind, Flipkart had actually shutdown its mobile website earlier so this is a welcome return to the web for them.

As far as URLs are concerned, the Chrome team agrees. Both Chrome and Opera are working on ways to fix it.

I find the third point fruitless to discuss as it seems to emanate from distrust of Google. I understand why people distrust Google, but distrust often makes for circular conversations because you can never address the core issue.

Which bring us to the final point and the question of whether or not we’re spending too much time worrying about native.

The web that cried wolf

In response to some of the progressive web apps criticism, Alex Russell went on a bit of a Twitter rant about the challenges facing the web. Peter Gaston collected the tweets and added some thoughts. Bruce Lawson from Opera chimed in saying that Alex “is not being alarmist; it's true.

And yet, the day after Alex issued a clarion call to save the web, Recode’s big headline is that The App Boom is Over:

Last month, the top 15 app publishers saw downloads drop an average of 20 percent in the U.S., according to research from Nomura.

Because this isn’t the first time that someone has sounded the web’s death knell, people were rightly skeptical.

I’ve been in the skeptical camp in the past. And I’ve previously ascribed a lot of the concern about the web from Chrome Googlers to their internal battles with the Android team.

It can’t be fun to be part of the Google’s Web Developer Relations team—recently renamed from Chrome Developer Relations to emphasize that their role is to promote the web, not one browser—and have the company you work for releasing Android Instant Apps. Worse, announcing it at a conference where you have to spend the next three days answering multiple questions about it from people like me. (Sorry!)

But the Google Web DevRel team also has more opportunity to talk to people working in companies around the world. Same with Opera. It would be foolish to completely dismiss what they say.

Plus, in addition to my fears of Android Instant Apps, I’ve recently been spending some time in Google Trends and the graphs for web-related searches are a bit depressing.

Web development vs. Android Development Google Trends

Web design Google Trends

Lately, I vacillate on whether the web is endangered or poised for a massive growth due to the web’s new capabilities. Frankly, I think there are indicators both ways.

Pascal’s Wager

A few years ago, I watched a video on climate change that applied a decision matrix to the question of whether or not climate change is real. The matrix looked something like this for worst case scenarios:

Climate Change Do Something Do Nothing
Not real Spend money unnecessarily, possible global recession Status quo
Real Save the world Destroy the world

That’s simplified, but it gets at the heart of the matter. If climate change deniers are right, the worst case scenario is a recession which sucks, but is something we’ve recovered from. If climate change deniers are wrong, the consequences for inaction are something that puts our existence as a species at risk.

I’ve been thinking about a similar matrix for this question of whether or not the web is imperiled by native and if we should do the things that Google advocates. That decision matrix might look like this:

Web is threatened Build PWAs Don’t Build PWAs
Not real Users get a faster, better web experience Current slow web
Real Users get a faster, better web experience, plus we save the web Web loses to native

I need to provide a few caveats on this table. First, I’m not equating people who say the web is fine with climate change deniers. The scientific evidence and consensus on climate change is overwhelming. Whereas I already mentioned that support for whether or not the web is endangered seems to change daily.

Second, I used progressive web apps here, but I’m using them as a placeholder for doing the work necessary to create faster and better experiences on the web. Basically, whatever we need to do to have less of this:

And more experiences that are so snappy—particularly on mobile devices—that people aren’t driven to native apps.

Build better experiences

When I think about the problem this way, the question of whether or not the web is imperiled doesn’t matter. The collection of web tools under the umbrella of Progressive Web Apps provide a better experience for people using our sites. This should be a no brainer.

Yes, none of the current examples of Progressive Web Apps are perfect. We have yet to see the Boston Globe or ESPN of Progressive Web Apps.

I see that as opportunity. An opportunity to build Progressive Web Apps that show how these tools can provide a better experience for everyone regardless of the device or browser they are using.

And I’ll say it again, I really want to work on the Boston Globe of Progressive Web Apps, so if you want to be that site, we should talk.

Whether you see the web as threatened or see Chicken Little in people’s fears and whether you like progressive web apps or feel it is a stupid Google marketing thing, we can all agree that putting energy into improving the experience for the people using our sites is always a good thing.

One amazing video that shows the potential of the physical web

Michael Mahemoff‘s recent article on how Progressive Web Apps have Leapfrogged the Native Install Model reminded me of a video I discovered while researching my new Adapting to Input talk.

I keep revisiting the video because it shows how the physical web can provide a substantially better experience than the native app install process.

Here’s the video from Matthew Sibigtroth:

Let’s compare the install process for the physical web versus what it would likely take using native apps.

Physical web install process

  1. Look in drawer for physical web beacons.
  2. Select physical web beacon.
  3. Web page launches. Prompts for pairing with toy.
  4. Select toy to complete pairing.
  5. Control toy.

There are two challenges in this process that will be solved with time. First, people need to know about physical web beacons. Second, the physical web and web bluetooth specifications are new and cross browser support is limited.

In fact, this process will likely be streamlined further in the future when the phone automatically notifies the user about nearby beacons.

Native app install process

  1. Confirm that the toy has an app for your platform.
  2. Open app store.
  3. Search for toy. Hope the toy has a unique name so you can find the app easily.
  4. Install the app and enter app store password if necessary.
  5. Wait for app to download.
  6. Open app.
  7. Pair with toy.
  8. Control toy.

Three fewer steps may not seem like much, but the total process is much slower. Apps are usually several megabytes in size making that step alone take much longer than the comparable step for the physical web.

Put a different way, it only takes 18 seconds from when the person first touches the screen to when the toy can be controlled. And that is with the person going slowly so that the people viewing the video can see each step.

It took me nearly 8 seconds on my phone to simply find, launch, and get to the home screen of the app store.

Yes, that will be faster for other people depending on placement of the app store icon, how recently they used the store, and network speed. But it is hard to envision that process ever being faster than simply following the physical web beacon.

The easier install process of the physical web and progressive web apps makes for an exciting future.

Service Workers at Scale, Part I: CDNs and Multiple Origins

We recently got the opportunity to develop a service worker for use on Smashing Magazine, which we’ll write about in more detail soon. This is the first of a multi-part article that will examine some of the solutions we arrived at and lessons we learned.

One of the challenges we’ve encountered while developing this worker is strategic caching for external resources. Most of the service worker examples we’ve seen address handling requests for local resources on the same domain. But pages served by complex sites often need to fetch (and cache) assets from external domains as well. Things can get complicated when these various domains need special handling, so we need a manageable way to structure our service worker to evaluate requests for both local and external resources.

Special handling for CDNs and subdirectories

The host of our service worker happens to be a WordPress site with assets requested from both the local server as well as CDNs. We need to handle fetch events differently depending on which domain or subdirectory they relate to. Some CDN requests need to be routed to cache functions, and some local requests (such as those for pages within the admin area) need to be ignored altogether. To fulfill this behavior, we need some way to designate rules for different URL origins.

One approach uses regular expressions. We need to match the URLs of fetch event requests against a set of base URLs, and using RegExp for this makes sense. In practice, this method works well initially, but maintenance becomes more of a concern as the patterns get more complex.

Take for example an entry to match “content pages”:

const CACHE_PATTERNS = {
  // ...
  contentPage: new RegExp(
    `^${self.location.origin}\/((?!wp-admin).*)$`
  ),
  // ...
};

This will match any local URLs except for ones with wp-admin following the first slash. It’s not a complicated pattern as-is, but what about when we need to add another subdirectory exception? Is there another approach that will cater more to maintainability and comprehension?

Comparing URLs with the URL class

Let’s substitute the great power and responsibility of regular expressions for a more explicit way to classify URLs. Using a Map structure to represent the base URLs to handle fetch events for, we can “flag” some items to indicate them as subdirectories to ignore:

const CACHE_FLAGS = {
  ignore: -1
};
 
const URL_MAP = new Map([
  ['/'],
  ['/wp-admin/', CACHE_FLAGS.ignore],
  ['/search-results/', CACHE_FLAGS.ignore],
  ['http://1.gravatar.com/'],
  ['https://media-mediatemple.netdata-ssl.com/wp-content/'],
]);

It’s more verbose, but it provides a clear interface for maintaining the list of base URLs we want to act on.

From this core list, we can derive more specific ones. Each of the derived lists consists of URL instances:

const URLS_TO_IGNORE = [];
const URLS_TO_HANDLE = [];
 
URL_MAP.forEach(function (flag, baseUrl) {
  const url = new URL(baseUrl, self.location);
  if (flag === CACHE_FLAGS.ignore) {
    URLS_TO_IGNORE.push(url);
  } else {
    URLS_TO_HANDLE.push(url);
  }
});

With our subjects of interest stored as URL instances, we can then use properties like origin and pathname in our logic:

function isIgnoredUrl (url) {
  // URL minus the query string and hash
  const urlBase = `${url.origin + url.pathname}/`;
  const isMatch = ({ origin, pathname }) =>
    urlBase.startsWith(origin + pathname);
 
  return URLS_TO_IGNORE.some(isMatch) // flagged "ignore"
    || !URLS_TO_HANDLE.some(isMatch); // altogether unlisted
}

Should this request be handled?

With our URL classifications and helper functions in place, the fetch event handler can use them to decide if it needs to intercept a request:

self.addEventListener('fetch', function (event) {
  const request = event.request;
  const url = new URL(request.url);
  const isGet = request.method === 'GET';
 
  // Requests we don't care to handle
  if (!isGet || isIgnoredUrl(url)) {
    event.respondWith(fetch(request));
    return;
  }
 
  // Requests we do care to handle
  event.respondWith(
    // Additional cache and/or fetch strategy
  );
});

Coming up next: Using URL properties for Offline fallbacks

In part two, we’ll take a look at another unique challenge this project presented: serving URL-aware offline fallbacks.

The Power of Responsive Design Sprints

In 2008, I sat gobsmacked in Web 2.0 Expo session. The topic of M. Jackson Wilkinson‘s presentation was “Design and User Experience in the Agile Process“, and I literally couldn’t imagine designing that way.

Fast forward seven years, add responsive design to the mix, and I can no longer imagine designing any other way. Responsive Design Sprints are key to our process.

Why agile for design?

Our path to agile for design started with moving to agile for development. Lyza and Megan spearheaded a move to agile development a few years ago and that change influenced the way we looked at projects.

Because of our success using agile for development, we were primed to try it when responsive design broke our existing design process.

We’ve found that agile design is ideally suited for designing responsive patterns. It allows us to focus on small pieces of a design and iterate on them quickly.

What is a Responsive Design Sprint?

Most of the time when people talk about design sprints, they’re referring to product design sprints. This is the process that Google Ventures, our friends at Fresh Tilled Soil and others have developed and promoted as a way to quickly validate product design and direction.

Thoughtbot's Product Design Sprints diagramThoughtbot’s Product Design Sprint Phases

Product design sprints are an exceptionally useful tool. The phases in our sprints have some similar characteristics—you’ll see parallels to the Understand, Diverge and Prototype phases in particular.

However, Responsive Design Sprints are decidedly different in what they’re trying to accomplish and diverge from Product Design Sprints in some specific ways.

Pre-sprint planning

Our first step when planning a project is to break the site down into the patterns—from the lowest level to higher-level components—that we need to design.

patterns-into-sprints

This is our starting position for each sprint, but because we’re working in an agile fashion, we’re flexible and can change what patterns are being worked on. We can also spend more time on patterns or bring patterns in from later sprints if we’re ahead of our plan.

Before each two-week sprint, we collaborate with our clients to figure out what patterns will be covered in the upcoming sprint. This not only allows us to confirm that our priorities are correct, but it also gives our clients a heads up on what they need to prepare for the sprint kickoff.

Sprint Day 1 — Client briefing and small screen sketching

Client briefing

Each sprint starts with a briefing on the patterns for that sprint. This briefing is led by the client team.

During the briefing, the client team provides everything they know about the patterns including:

  • How the patterns are used.
  • Any edge cases for the patterns.
  • Analytics information on the patterns.
  • User testing that has been done.
  • Support or user feedback on patterns.

If we’re dealing with foundational patterns like typography and buttons, the briefing could simply be going over any existing brand and style guidelines.

However, when we start working on more complex patterns—for example a shopping cart interface—there is often quite a bit of information about the pattern to convey.

Small screen sketching

After the briefing, we start group sketching sessions. Ideally, we do this with our client, but often we work with teams in other parts of the world so we only get to sketch together a couple of times during a project.

Everyone participates in the sketching irrespective of roles. Sketching small screen designs helps people start thinking more tangibly about the constraints of small screens. It is one of the reasons why we include clients in sketching exercises whenever we can.

We pick the portions of the patterns that we think are going to be the most difficult to fit on small screens and sketch those. This often means that we’re not sketching the entire interface or even the entire pattern that we’re looking at, but instead just a small portion of the pattern that looks particularly troublesome.

Sketches on sticky notes

We sketch on small sticky notes using Sharpies. Every sketching session is time-boxed. The constraints on time, size and low fidelity markers prevent people from getting too detailed in their drawings.

And we use Sharpie brand markers because Tyler is a marker snob.

(Tyler here: The store-brand ones bleed too much!!)

Each person posts their sketches on the board and talks about what they saw and how they were trying to solve the problems. The conversation around the sketches is just as important as the sketching itself.

Photo of me presenting some of my terrible sketches

The goal of the sketching session is to give the designers some ideas on how they might solve the problems that the patterns represent. We do multiple sketching sessions until people feel they have enough material to explore.

Focus on small screens first

For most of the sprint, we focus on small screens. We’re often asked how things will work on wider screens early in a sprint, but we try to resist thinking about that yet.

If you’ve managed to organize your life to fit inside a New York City apartment, you’re not going to have any trouble adjusting to a big house in the suburbs. The same is true of responsive designs.

If you nail the small screen design, the larger sizes will be easy by comparison.

Sprint daily activity — Quick design iterations

We move immediately from sketches to testing our ideas in the browser. We create quick prototypes of the designs that we then share with the client team in Slack.

The video above shows collaboration happening on day 2 of a new project. We’re showing simple small screen designs in the browser and getting feedback both from our co-workers and the client team.

Often, we will have members of the client team working on patterns as well. They may be sharing mockups, photographing sketches, or even coding prototypes.

What matters most is that we’re sharing ideas quickly and iterating towards better solutions.

LICECap, the worst named, best tool ever

One of the advantages of designing in the browser is that we’re able to demonstrate how things should interact. But when we’re working with distributed teams, we need a tool to show the interaction.

That’s where LICECap comes in. LICECap makes it easy to capture screen video and save it as an animated gif.

So instead of saying to a client, “imagine we hide all of the controls behind a single button until someone clicks on it” and hoping that the client understands, we simply post an animated gif in Slack.

Animated gif showing add to cart interaction

The animated gif eliminates confusion and facilitates quick feedback.

Mid-sprint review meetings

The amount and frequency of review meetings in a sprint change from project to project, but generally we’ll have one review meeting late in the first week of the sprint to make sure we’re on the right track.

We share our screens and show any in-progress work. Those who have been active in our shared Slack channel will have seen most of the work already, but the meeting ensures that all stakeholders are looking at the patterns early in the sprint.

Depending on the complexity of the pattern, by the second review meeting we’ll have examples of how the pattern will adapt as the screen size increases.

Sample two-week sprint schedule

End of sprint review

At the end of the two week period, we have one final meeting to review the progress on the patterns tackled during the sprint. Nothing in this meeting should be a surprise because we’ve been sharing our work throughout the sprint.

After we’ve reviewed the patterns, we talk about what worked well during that sprint and what we could improve on for the next sprint. We’re not only iterating on the designs, but on the process itself.

This isn’t rocket science, but it feels new just the same

Nothing I’ve described here is new.

Agile processes have been around for years. Many people have talked about how responsive design makes pattern libraries more important than ever.

But the combination of responsive design, patterns libraries, sprints, gifs, and constant communication via Slack creates a radically different design process. Even if the pieces that comprise them aren’t new, Responsive Design Sprints certainly are.

Most importantly, Responsive Design Sprints work exceptionally well.

They helped us convert complex interfaces that seemed insurmountable. When we were asked to convert an e-commerce design to responsive in only three months, it was Responsive Design Sprints that helped us move quickly and finish the project a week ahead of schedule.

I can only imagine how stunned 2008 me would be to see what our design process looks like now.