The Evolution of Frontend Paradigms

Frontend frameworks and libraries have evolved across generations to increasing abstraction, let's have a look at how that happened, and what each new generation offers over the previous. 4 July 2019

# Background

Web browsers use a combination of 3 different technologies to display a webpage: HTML, JS, and CSS. Firstly, HTML is used to setup the initial DOM . From there, JS can be used to further modify the DOM according to user interactions, achieving a highly interactive experience. Similarly, CSS is used to setup the initial CSSOM , which can also be dynamically updated with JS.

Here, we consider the evolution of paradigms on how the DOM can be manipulated. This treatise increases in level of abstraction, but is only approximately linear in time.

When browsers navigate from one page to the next, it reloads the entire HTML + JS + CSS. This ensures that every page has a clean slate, but is a very expensive operation. Turbolinks  makes the navigation between pages faster, by keeping the JS + CSS state, and only modifying the DOM wholesale by using the innerHTML API .

A naive implementation might look like the following. But the full implementation also performs caching, history pushState , and more.

document.querySelectorAll("a").forEach(function (link) {
link.addEventListener("onclick", async function () {
const html = await fetchAndExtractBody(link.getAttribute("href"))
document.documentElement.innerHTML = html
})
})

A fork called Turbograft  allows the ability to add partial replacements, but otherwise still using innerHTML.

# jQuery

Browsers also provide APIs like createElement()  to directly and surgically update the DOM. However, due to its verbosity, and the fact that there are many browser specific nuances, libraries like jQuery  emerged in the early days of web development to abstract and simplify its use.

State such as user input, attributes such as class names and styles, etc. are stored in the DOM. jQuery merely provides an abstraction to make it easy to manipulate the state.

$("#button-container").on("click", function (event) {
$("#banner-message").show()
})

# Stimulus

However, jQuery is difficult to scale, as callback functions essentially live in a global hyper-space. Enter Stimulus  as a component abstraction over jQuery. It introduces component namespacing, and lifecycles such as connect() and disconnect(). Whenever the HTML is updated, say via browser APIs like innerText  or innerHTML , the MutationObserver API  checks for changes in the DOM, and connects / disconnects the relevant Stimulus controller. State remains in the DOM.

Naturally, Stimulus relies on server rendered HTML, or some other form of string concatenation to update the DOM.

Here's the HTML and JS snippets, taken from Stimulus's website.

<div data-controller="hello">
<input data-target="hello.greet" type="text">
<button data-action="click->hello#greet">Greet</button>
<p data-target="hello.output"></p>
</div>
import { Controller } from "stimulus"

export default class extends Controller {
static targets = [ "greet", "output" ]

connect() {
console.log("connected")
}

disconnect() {
console.log("disconnected")
}

greet() {
this.outputTarget.textContent = `${this.greetTarget.value} World!`
}
}

# Mustache / Handlebars

The paradigms above still relied on server generated HTML (or fragments of), which suffers from network latency. To improve performance, the browser needed to generate its own inner HTML. Mustache  and Handlebars  are such libraries that generate HTML using string interpolation.

document.getElementById("target").innerText = Mustache.render(
"The Evolution of Frontend Paradigms spends ",
{ title: "Joe", calc: () => 2 + 4 }
)

# Hyperscript

The above paradigms leant heavily on browser APIs to update the DOM, either surgically with document.createElement()  or wholesale with innerHTML . Upon changes to the DOM, the MutationObserver  API calls the Javascript to attach behaviour.

Hyperscript  allows a more programmatic way of manipulating the DOM. Unlike the methods above, state has now moved into JS, and the DOM becomes a reflection of that state.

import h from "hyperscript"

h("div#page",
h("div#header",
h("h1.classy", "h", { style: {"background-color": "#22f"} })
),
h("div#menu", { style: {"background-color": "#2f2"} },
h("ul",
h("li", "one"),
h("li", "two"),
h("li", "three")
)
)
)

A naive implementation might look like the following. Notice how we're using browser APIs to modify the DOM. For server side rendering, the implementation to generate strings should also be similarly obvious.

function h (tag, children, attrs = {}) {
let element, classes
if (tag.startsWith(".")) {
element = "div"
classes = tag.split(".").filter(t => t)
} else {
[element, ...classes] = tag.split(".")
}

const node = window.document.createElement(element)
classes.forEach(klass => node.classList.add(klass))

for (const prop in attrs) {
if (typeof attrs[prop] === "function") {
node.addEventListener(prop, attrs[prop])
} else {
node.setAttribute(prop, attrs[prop])
}
}

const append = appendChild.bind(node)
if (Array.isArray(children)) {
children.forEach(append)
} else {
append(children)
}

return node
}

# React / Inferno / Preact

Directly manipulating the DOM on every change is expensive, as browsers have to recompute layout. As such, the notion of a virtual DOM is introduced as an intermediate place to store the desired DOM. Then, at convenient times, the virtual DOM is flushed to the real DOM. React  is the poster child of such a paradigm.

Fundamentally, React is just Javascript. Notice the return function below looks very similar to the Hyperscript example. As such, concepts such as higher order components and render props are simply currying and callback functions in disguise - these are staple Javascript techniques.

Finally, because state is stored in Javascript, React encourages one directional binding in a flux pattern.

function GreetWorld ({ greeting }) {
const [ count, setCount ] = useState(0)

useEffect(function () {
console.log("mounted")

return function () {
console.log("unmounted")
}
}, [])

return React.createElement("p", {}, `${greeting} World!`)
}

# JSX

Writing React with React.createElement(), or indeed writing hyperscript can be difficult to read, with errors in comma placements and parentheses resulting in erroneous output. As such JSX is introduced as syntax sugar to make it look like HTML. But remember, it is ultimately just Javascript.

function GreetWorld ({ greeting }) {
return <p>{greeting} World!</p>
}

# Angular / Aurelia / Vue

Extending on the idea of using syntax sugar, template based libraries like Angular  and Vue  create another layer of abstraction on top of Javascript with custom Domain Specific Languages .

These DSLs look like HTML, but have extensions like stores, route handling, two directional input binding, the ability to write to <head />, and so on. It also has iterators, switches, and other similar capabilities mixed in as directives. Ultimately, these DSLs generate Javascript, so these frameworks also provide escape hatches into raw Javascript, such that techniques like currying, callback functions, and other specialised behaviour can be enabled.

This example below is written in Vue.

<template>
<p> World!</p>
</template>

<script>
export default {
data () {
return { greeting: "Hello" }
},
mounted() {
console.log("mounted")
},
beforeDestroy() {
console.log("beforeDestroy")
}
}
</script>

# Svelte

With DSLs that look like HTML but with JS hooks, it now becomes possible to bypass the generation of an intermediate JS representation, and to bypass the virtual DOM entirely . All of that calculation can be statically pushed into the compiler to generate code that can surgically update the DOM with virtually no overhead.

Such is the promise of Svelte .

<script>
import { onMount, onDestroy } from "svelte"
import { count } from "stores"

onMount(() => console.log("mounted"))
onDestroy(() => console.log("destroyed"))
</script>

<p>The count is {$count}</p>
<button on:click={count.increment}>+</button>
<button on:click={count.decrement}>-</button>
<button on:click={count.reset}>reset</button>

# Next.js / Nuxt.js / Sapper

Extending the idea of DSLs even further, and recognising that web URLs have a lot in common with the file/folder directory structure, it is now possible to remove a lot of boiler plate code; requiring the developer to only place well-named files in well-named folders. The rest of the building of Single-Page-Applications, Server-Side-Rendering, code-splitting, service-workers, hot-loading, and so on can be abstracted and automated.

This convention-over-configuration paradigm makes common things easy, but can also cause uncommon things to become much harder.

# Summary

HTML is only used to set the initial DOM, or to update the DOM wholesale with innerHTML. For more surgical updates to the DOM, the browser provides other more granular APIs to create and update one element at a time.

State could be stored in the DOM, and the first half of this article discusses simple paradigms on achieving a highly interactive experience, leaning as much as possible on browser APIs.

But state could also be stored in Javascript, and the DOM is mainly used as an output artifact. The second half of the article saw more and more declarative ways of specifying how the DOM should be updated, until eventually DSLs were mature enough to fully abstract the entire process of building web applications.

Every paradigm is still relevant today, and all are still actively developed. Whilst it is tempting, especially for newcomers to focus only on the latest and trendiest, I think learning from every one of these paradigms help improve code quality, and demonstrates real understanding of what building web applications is all about.