Let’s start with a brief, mostly subjective history of web tech.
A long long time ago, web development was pretty much a single discipline where any developer would work throughout their stack. Most would be armed with a backend language or framework, HTML and CSS, with a sprinkle of JavaScript.
Then the demands for richer and richer user interactions in web applications pushed us in a very different direction. Front-end development became its own discipline and a bunch of new technologies emerged.
The front-end framework overloadpermalink
On a semi-regular schedule, backend specialists would read news about the latest and greatest frontend framework / metaframework / library and increasingly channel their inner Brenda from Bristol shouting “You’re joking! Not another one!”
Something broke in the community of folks that have long identified as full-stack web developers, and the quest for a different model started. The mission: find a technology based on a single programming language to drive the backend business logic and frontend user interaction.
The pendulum shifts againpermalink
After some toying with isomorphic apps, two paradigms emerged that allow full-stack developers to take back control of their stacks:
- React everywhere, such as Next.js and Remix
- Backend-driven frontend, such as Phoenix LiveView (LiveView) and htmx
Enter LiveViewpermalink
LiveView is built on top of the Phoenix web framework, and leverages some interesting features of Elixir — the language it’s written in — along with Erlang and its virtual machine, the BEAM. Its model inspired similar frameworks: HotWire in the Ruby/Rails world, LiveWire in PHP and Laravel.
Some big users of LiveView are cars.com and Fly.io (who as of October 2024 employs Chris McCord, the author of Phoenix and LiveView).
How does LiveView workpermalink
Let’s have a look at what happens when a user hits a LiveView enabled page.
You can follow along and admire my web design skills by having a look at a very simple example app I wrote and its source code.
Check it outStep 1: Initial render
The first request-response cycle is the same you’d encounter with any classic web framework. The user asks for an HTTP resource, the server stitches together some HTML and ships it back to the browser.
This enables the page to be fully rendered no matter what comes next, which has nice properties for search engines (and any other manner of dodgy crawler… I’m looking at you, LLMs trainer bots) and can enable some basic usage even on bad network or low bandwidth conditions. Y’know, progressive enhancement.
The dynamic content gets handled in the same way by both the initial render and the LiveView first render:
- Code language
- html
<div> Count: <%= @count %> </div>
In the Phoenix parlance, @count
is called an “assign”. It gets calculated in the mount
function and injected in the template:
- Code language
- elixir
def mount(_params, _session, socket) do {:ok, socket |> assign(:count, 0)} end
In this case, count
is the integer 0
, but assigns can be any valid data structure: a list, a map, a struct… The sky’s the limit. Well, the sky, and your server’s RAM.
Step 2: The View becomes Live
The page that gets delivered has a sprinkle of JS: most importantly, it carries the code to request a WebSocket connection to the server. The HTML gets re-rendered and a “process” — such as a lightweight thread — starts on the server to store the current state of the view the user is engaging with. For all intents and purposes, the app is now stateful.
Depending on the complexity of such processes, Elixir can handle a lot of them on a single machine, fairly efficiently. It takes a lot of beating before the user experience is actually degradated thanks to the BEAM’s scheduler.
Step 3: The reactive cycle begins
How does the app becomes interactive? By providing bindings on the page. For example, a DOM element with the phx-click="increment"
HTML attribute informs the small JavaScript library to push the increment
event up the WebSocket whenever it’s clicked.
- Code language
- html
<div> <button type="button" phx-click="increment">+</button> <button type="button" phx-click="decrement">-</button> </div>
The LiveView then handles the event, and adds 1 to the count.
- Code language
- elixir
def handle_event("increment", _, socket) do count = socket.assigns[:count] {:noreply, socket |> assign(:count, count + 1)} end
If the state causes the rendering to change — which will happen in this case as the assign @count
will change — the LiveView process pushes the relevant, minimal diffs back to the browser via the WebSocket. Here’s an example from the network inspector:
- Code language
- plaintext
up: ["4","13","lv:phx-F_lyW6ngKkiixRwB","event",{"type":"click","event":"increment","value":{"value":""}}] down: ["4","13","lv:phx-F_lyW6ngKkiixRwB","phx_reply",{"status":"ok","response":{"diff":{"1":{"0":"1"}}}}]
In this case {"diff":{"1":{"0":"1"}}}
tells the client JavaScript library to replace the slot 0
(the <%= @count %>
tag we saw above) with just the content 1
. Obviously more complex apps will pass more complex diffs, but the basics are the same.
The JavaScript library then updates the DOM, and now we’re ready for the next event to trigger.
What else does it dopermalink
On-browser commands
LiveView.JS
module allows you to write simple but extensive browser-based CSS manipulations without the need of a roundtrip over the web socket. Hiding and showing DOM elements or toggling classes is trivial and can be still controlled without writing a single line of JavaScript.
A lot of heavy lifting for you
Think of a React app: you need to stitch your frontend and backend together with a bunch of things to make it work, like auth, an API layer, a separate build pipeline, ideally some tests. LiveView takes away all the glue work, and you can focus on delivering features instead.
Session tracking across the cluster
One of the most interesting aspects of using the BEAM, is that you can cluster all your servers together and allow module calls across them. This is referred in the community as Distributed Erlang. If you do build such cluster, and a user’s session gets lost and then reconnected with a different server, the new server can fetch the state from the previous one.
Wrapping uppermalink
LiveView can empower developers to create rich apps by focusing on a single programming language and a single framework (or a framework and a half, more realistically, if we count both Phoenix and LiveView). With LiveView, you can cut down on a lot of glue work, and it supports progressive enhancement where useful: if you need a non-JavaScript fallback, you can build a non-LiveView, traditional View-Controller route in Phoenix.
It’s not all unicorns and flowers though. While LiveView’s approach works for the vast majority of simple apps, something like Google Maps is too complex for it to handle. Similarly, offline support is out of the question. Lastly, LiveView is extremely niche compared to React; the resources available are proportionally scarce.
A glimpse of simplicity
I hope this gave you an overview on what happens in some dark corners of the web that aren’t following the herd that brought us the current set of mainstream unwieldy technologies. I’m increasingly optimistic about the direction web frameworks are taking in terms of productivity and I’m excited about the industry adoption of these new patterns.