Progressive enhancement

Stimulus Handbook: Designing a Resilient User Interface

We should also expect people to have problems accessing our application from time to time. For example, intermittent network connectivity or CDN availability could prevent some or all of our JavaScript from loading.

It’s tempting to write off support for older browsers as not worth the effort, or to dismiss network issues as temporary glitches that resolve themselves after a refresh. But often it’s trivially easy to build features in a way that’s gracefully resilient to these types of problems.

This resilient approach, commonly known as progressive enhancement, is the practice of delivering web interfaces such that the basic functionality is implemented in HTML and CSS, and tiered upgrades to that base experience are layered on top with CSS and JavaScript, progressively, when their underlying technologies are supported by the browser.

Stimulus: A modest JavaScript framework for the HTML you already have

Stimulus is a JavaScript framework with modest ambitions. It doesn’t seek to take over your entire front-end—in fact, it’s not concerned with rendering HTML at all. Instead, it’s designed to augment your HTML with just enough behavior to make it shine. Stimulus pairs beautifully with Turbolinks to provide a complete solution for fast, compelling applications with a minimal amount of effort.

[…]

Sprinkle your HTML with controller, target, and action attributes:

Code language: HTML

<!--HTML from anywhere-->
<div data-controller="hello">
  <input data-target="hello.name" type="text">
 
  <button data-action="click->hello#greet">
    Greet
  </button>
 
  <span data-target="hello.output">
  </span>
</div>

Write a compatible controller and watch Stimulus bring it to life:

Code language: JavaScript

// hello_controller.js
import { Controller } from "stimulus"
 
export default class extends Controller {
  static targets = [ "name", "output" ]
 
  greet() {
    this.outputTarget.textContent =
      `Hello, ${this.nameTarget.value}!`
  }
}

Turbolinks

Turbolinks® makes navigating your web application faster. Get the performance benefits of a single-page application without the added complexity of a client-side JavaScript framework. Use HTML to render your views on the server side and link to pages as usual. When you follow a link, Turbolinks automatically fetches the page, swaps in its <body>, and merges its <head>, all without incurring the cost of a full page load.

  • Optimizes navigation automatically. No need to annotate links or specify which parts of the page should change.
  • No server-side cooperation necessary. Respond with full HTML pages, not partial page fragments or JSON.
  • Respects the web. The Back and Reload buttons work just as you’d expect. Search engine-friendly by design.
  • Supports mobile apps. Adapters for iOS and Android let you build hybrid applications using native navigation controls.

Spoiler: server-rendered HTML can work offline

The architecture astronauts who, for the past decade, have been selling us on the necessity of React, Redux, and megabytes of JS, cannot comprehend the possibility of building an email app in 2020 with server-rendered HTML 😴

[…]

The effects are truly toxic. Last decade’s obsession with SPAs has poisoned the minds of even the brightest teachers in our industry.

Like, there’s no way this stuff can work offline, right?!

Briefly read up on how HEY is implemented. Is this a correct summary of the pros and cons of its [server]-centric approach?
– Pro: Works fast on older devices.
– Con: Can’t be used offline.

Hey now

Progressive enhancement is at the heart of everything I do on the web. It’s the bedrock of my speaking and writing too. Whether I’m writing about JavaScript, Ajax, HTML, or service workers, it’s always through the lens of progressive enhancement. Sometimes I explicitly bang the drum, like with Resilient Web Design. Other times I don’t mention it by name at all, and instead talk only about its benefits.

I sometimes get asked to name some examples of sites that still offer their core functionality even when JavaScript fails. I usually mention Amazon.com, although that has other issues. But quite often I find that a lot of the examples I might mention are dismissed as not being “web apps” (whatever that means).

The pushback I get usually takes the form of “Well, that approach is fine for websites, but it wouldn’t work something like Gmail.”

It’s always Gmail. Which is odd. Because if you really wanted to flummox me with a product or service that defies progressive enhancement, I’d have a hard time with something like, say, a game (although it would be pretty cool to build a text adventure that’s progressively enhanced into a first-person shooter). But an email client? That would work.

[…]

Can you build something that works just like Gmail without using any JavaScript? No. But that’s not what progressive enhancement is about. It’s about providing the core functionality (reading and writing emails) with the simplest possible technology (HTML) and then enhancing using more powerful technologies (like JavaScript).

Progressive enhancement isn’t about making a choice between using simpler more robust technologies or using more advanced features; it’s about using simpler more robust technologies and then using more advanced features. Have your cake and eat it.

Fortunately I no longer need to run this thought experiment to imagine what it would be like if something like Gmail were built with a progressive enhancement approach. That’s what HEY is.

Sam Stephenson describes the approach they took:

HEY’s UI is 100% HTML over the wire. We render plain-old HTML pages on the server and send them to your browser encoded as text/html. No JSON APIs, no GraphQL, no React—just form submissions and links.

If you think that sounds like the web of 25 years ago, you’re right! Except the HEY front-end stack progressively enhances the “classic web” to work like the “2020 web,” with all the fidelity you’d expect from a well-built SPA.

See? It’s not either resilient or modern—it’s resilient and modern. Have your cake and eat it.

And yet this supremely sensible approach is not considered “modern” web development:

The architecture astronauts who, for the past decade, have been selling us on the necessity of React, Redux, and megabytes of JS, cannot comprehend the possibility of building an email app in 2020 with server-rendered HTML.

[…]

Their focus is very much on people above technology. They’ve taken a human-centric approach to their product and a human-centric approach to web development …because ultimately, that’s what progressive enhancement is.

Always bet on HTML

In September of 2019, Zach Leatherman tweeted

Which has a better First Meaningful Paint time?

  1. a raw 8.5MB HTML file with the full text of every single one of my 27,506 tweets
  2. a client rendered React site with exactly one tweet on it

(Spoiler: @____lighthouse reports 8.5MB of HTML wins by about 200ms)

Take a moment to wrap your head around that.

It’s perceivably faster to load 8.5 megabytes of HTML than it is to load a single tweet with a client-side React app.

[…]

Real companies can and do build apps with HTML first

The folks at Basecamp just released a new email product, Hey, that tries to address a lot of the stuff that people find frustrating about email.

Neither product is really my cup of tea, but what I find super interesting is how Hey is built.

It’s core is server-rendered HTML. Basecamp is a Ruby on Rails shop (their CTO created Rails). Almost every view in the app is created on a server.

Then, they sprinkle just a little vanilla JS on top to turn things up to 11.

Basecamp uses a project they open sourced called Turbolinks. This JavaScript plugin intercepts link clicks and progressively enhances a server-side app into a single-page app (or SPA) by fetching additional pages with Ajax and only replacing the stuff that needs updating.

By using this approach, if the JS fails or isn’t supported, the app still loads and works and gives people the full experience. It also means you don’t have to wait for the full JS package to load before you can start using the app.

You still get the benefits of faster page loading that SPAs sometimes give you, but you don’t have to maintain two code bases or do complicated server-to-client hand offs (“rehydration” as they call it in the React world).

Cutting the mustard with only CSS

JavaScript can be pretty brittle, so having a way to exclude browsers that don’t cut the mustard via CSS can be really useful, especially if you don’t want to serve them large amounts of CSS that they won’t properly understand. Since we can’t prevent loading a stylesheet via feature queries, the media attribute on a <link> element seems the next best thing. Andy Kirk has come up with a few combinations:

Code language: HTML

<!--
  Print (Edge doesn't apply to print otherwise)
  IE 10, 11
  Edge
  Chrome 29+, Opera 16+, Safari 6.1+, iOS 7+, Android ~4.4+
  FF 29+
-->
<link rel="stylesheet" href="your-stylesheet.css" media="
  only print,
  only all and (-ms-high-contrast: none), only all and (-ms-high-contrast: active),
  only all and (pointer: fine), only all and (pointer: coarse), only all and (pointer: none),
  only all and (-webkit-min-device-pixel-ratio:0) and (min-color-index:0),
  only all and (min--moz-device-pixel-ratio:0) and (min-resolution: 3e1dpcm)
">
 
<!--
  Print (Edge doesn't apply to print otherwise)
  Edge, Chrome 39+, Opera 26+, Safari 9+, iOS 9+, Android ~5+, Android UCBrowser ~11.8+
  FF 47+
-->
<link rel="stylesheet" href="your-stylesheet.css" media="
  only print,
  only all and (pointer: fine), only all and (pointer: coarse), only all and (pointer: none),
  only all and (min--moz-device-pixel-ratio:0) and (display-mode:browser), (min--moz-device-pixel-ratio:0) and (display-mode:fullscreen)
">

Serving old browsers limited JavaScript and CSS

The Guardian website viewed with Internet Explorer 8: a very basic document with little to no CSS applied.
The Guardian navigation as seen in Internet Explorer 8. Unsophisticated yet functional.
nature.com as viewed with Internet Explorer 9: a very simple layout without much CSS applied.
The nature.com homepage as seen in Internet Explorer 9.

Since we’re still stuck with a small percentage of users still on various versions of Internet Explorer and other older browsers, a good way to deal with those seems to be to only serve most or all of our JavaScript and CSS to browsers that cut the mustard, leaving the older set with a basic but functional experience, without risk that our newer, shiny stuff will inevitably break, or the need for polyfills that may or may not work.

See also: Cutting the mustard with only CSS

1% or 13 million JavaScript requests per month to BuzzFeed time out

Ian Feather standing on a stage in front of a graphic that reads "13 million JavaScript requests will fail".

More evidence that we don’t fully control our web pages and that a non-zero number of page views don’t execute JavaScript fully or correctly, despite it being enabled.

Says @ianfeather at #AllDayHey — “our monitoring tells us that around 1% of requests for JavaScript on BuzzFeed timeout. That’s around 13 million requests per month.” A reminder if one were needed that we should design for resilience

Duck typing - Robust Client-Side JavaScript

As a weakly typed language, JavaScript performs implicit type conversion so developers do not need to think much about types. The concept behind this is called duck typing: “If it walks like a duck and quacks like a duck, it is a duck.”

typeof and instanceof check what a value is and where it comes from. As we have seen, both operators have serious limitations.

In contrast, duck typing checks what a value does and provides. After all, you are not interested in the type of a value, you are interested in what you can do with the value.

[…]

Duck typing would ask instead: What does the function do with the value? Then check whether the value fulfills the needs, and be done with it.

[…]

This check is not as strict as instanceof, and that is an advantage. A function that does not assert types but object capabilities is more flexible.

For example, JavaScript has several types that do not inherit from Array.prototype but walk and talk like arrays: Arguments, HTMLCollection and NodeList. A function that uses duck typing is able to support all array-like types.

Quoted content by Mathias Schäfer is licensed under CC BY-SA. See the other snippets from Robust Client-Side JavaScript.