How We Got to LiveView

I'm Chris McCord. I work at Fly.io and created Phoenix, an Elixir web framework. Phoenix provides features out-of-the-box that are difficult in other languages and frameworks. This is a post about how we created LiveView, our flagship feature.

LiveView strips away layers of abstraction, because it solves both the client and server in a single abstraction. HTTP almost entirely falls away. No more REST. No more JSON. No GraphQL APIs, controllers, serializers, or resolvers. You just write HTML templates, and a stateful process synchronizes it with the browser, updating it only when needed. And there's no JavaScript to write.

Do you remember when Ruby on Rails was first released? I do. Rails was also a revolution. It hit on the idea of using an expressive language and a few well-chosen, unifying abstractions to drastically simplify development. It's hard to remember what CRUD app development was like before Rails, because the framework has been so influential.

Today, I work in a language called Elixir. I spend my days building Phoenix, which is Elixir's goto web framework. Unlike Rails, Phoenix is more than just an Elixir web framework. In the process of building Phoenix, I believe we've hit on some new ideas that will change the way we think about building applications in much the same way Rails did for CRUD apps.

That's a big claim. To back it up, I want to talk you through the history of Phoenix, what we were trying to do, and some of the problems we solved along the way. There's a lot to say. I'll break it down like this:

  • How real-time app features got me thinking about syncing updates, rather than rendering static content, as the right primitive for connecting app servers to browsers.
  • How trying to build sync.rb with Ruby led me to Elixir, and why I think Elixir is uniquely suited to solve these problems.
  • How working in Elixir lit up "distributed computing", not as a radioactive core of a complicated database but as a basic building block for any web application.
  • How distributed computing and the ideas from sync.rb culminated in a Phoenix feature called LiveView.
  • And, finally, what we've been doing with these building blocks we've come up with.

Let's get started.

2013: Ruby on Rails

I think it's safe to say that there wouldn't be a Phoenix framework if I'd gotten the job I applied for at 37signals in 2013. I liked building with Rails, and I'd doubtless be working on new Rails features instead of inventing Phoenix.

That's because Rails is an amazingly productive framework for shipping code. It's about the flow. When you build something the Rails team had in mind, the flow is powerful and enjoyable. Rails people have a name for this; they call it "The Golden Path".

But when you need "real-time" features, like chat or activity feeds, you're off the Golden Path and out of the flow. You go from writing Ruby code with Ruby abstractions to Javascript and a whole different set of abstractions. Then you write a pile of HTTP glue to make both worlds talk to each other.

So, I had a simple idea: "what if we could replace Rails' render with sync and the UI updates automatically?". A few days later, I had sync.rb.

Sync.rb works like this: the browser WebSockets to the server, and as Rails models change, templates get re-rendered on the server and pushed to the client. HTML rendered from the server would sign a tamper-proof subscription into the DOM for clients to listen to over WebSockets. The library provides JavaScript for the browser to run, but sync.rb programmers don't have to write any themselves. All the dynamic behavior is done server-side, in Ruby.

This actually worked! Kind of.

Sync.rb provides a concurrent abstraction, and Rails wasn't especially concurrent. So I built on top of EventMachine, which is a Ruby/C++ library that runs an IO event loop. EventMachine is a useful library, but it has an uneasy relationship with core Ruby and its ecosystem, most of which doesn't expect concurrent code.

My EventMachine threads would silently die without errors. I had to check if the EventMachine thread had secretly died, and restart it, for every call, for every user, every time we wanted to async publish updates. I wanted to build dynamic features in a stack I knew and loved, but I didn't have the confidence the platform could deliver what I was trying to build.

Still, it worked. I had a minimal viable demo, and knew the approach could work if I could find a way to make it reliable. I looked to see how other languages addressed the "real-time" problem at scale. I found Elixir.

Elixir is José Valim's developer-focussed language built on the Erlang VM. Erlang is a battle-tested language designed for telecom applications, notable for the quality and reliability of the virtual machine it runs on. Erlang powered WhatsApp, which served 10 million users per server. The $19B Facebook acquisition was a nice calling card as well.

One look at Elixir and I was instantly hooked. I saw all the power and heritage of the Erlang VM, but also modern features like protocols, the best build tool/code runner/task runner I'd ever seen, real macros, and first-class documentation. All it needed was a web framework.

2014-2015: Phoenix

I created Phoenix to build real-time applications. Before it could even render HTML, Phoenix supported real-time messaging. To do that, Phoenix provides an abstraction called Channels.

To understand Channels, you need to know a few things about Erlang. Concurrency in the Erlang VM is first-class. Erlang apps are comprised of "processes", which are light-weight threads that communicate with messages rather than shared memory. You can run millions of processes in a single VM. Erlang is famously resilient: processes can exit (or even crash) and restart seamlessly. Processes message each other with abstractions like mailboxes and PubSub; messages are routed transparently between servers.

Channels exploit Elixir/Erlang messages to talk with external clients. They're bidirectional and transport-agnostic.

Channels are usually carried over WebSockets, but you can implement them on anything – the community even showed off a working Telnet client.

In a web app, a browser opens a single WebSocket, which multiplexes between channels and processes. Because Elixir is preemptively scheduled, processes load-balance on both IO and CPU. You can block on one channel, transcode video on another, and the other channels stay snappy.

This is starkly different from how Rails applications work. Rails uses "processes" too, but they're the "heavy" kind. Generally, a Rails request handler monopolizes a whole OS process, from setup to teardown. Rails is "concurrent" in the sense that the underlying database is concurrent, and multiple OS processes can update it. This complicates things that should be simple. For instance, running a timer that fires events on an interval requires a background job framework, and a scheme for relaying messages back through persistent storage. It's a level of friction that changes and limits the way you think about building things.

In Phoenix, Channels hold state for the lifetime of a WebSocket connection, and can relay events across a server fleet. They scale vertically and horizontally.

In November 2015, we put the Elixir/Erlang promise to the test. We load-tested Channels, sending 2 million concurrent WebSocket connections to a single Phoenix server, broadcasting chat messages between all the clients. It took 45 client servers just to open that many connections. Our 128GB Phoenix server sill had 45GB of free memory. We'd have run more clients, but we stopped when we ran out of file descriptors supported by our Linux kernel.

Phoenix did what sync.rb couldn't. Millions of concurrent websocket users with trivial user land code. We knew we had the foundation for a developer friendly real-time application framework. We just needed to figure out how to make it sing.

2016-2017: Presence

In 2016, developers didn't usually think they were building real-time applications. "Real-time" generally made people think of WhatsApp and Twitter and Slack. Many didn't have a firm idea of why they'd even want real-time, or the costs were too high to implement such things. Still, everyone had a need for standard web apps. The core team spent a lot of time making Phoenix a great MVC framework for building conventional database based web apps. But my head was still in real-time features.

So we released Phoenix Presence, a distributed group manager with metadata backed by CRDTs. Presence broadly solves the "Who's currently online?" feature.

Imagine your boss asks you to display how many other visitors are viewing a nearly sold out item to convert sales, or your client asks how hard it would be to show which team members are currently online. You no longer have to figure these things out. Apps become more interesting with live presence features. In fact, most collaborative apps get more interesting with this feature, even if they aren't "real-time" in any other way.

There are two important things to understand about Presence. The first is that it just works, with self-healing and without dependencies on external message queues or databases. Elixir has all this built in and Presence takes full advantage. You don't have to think about how to make it work.

The second is that Presence exploits a powerful, general abstraction. CRDT-based statekeeping has applications outside of online buddy lists. Presence gets used in IOT apps to track devices, cars, and other things. If you don't have something like Presence, you probably don't think to build the kinds of features it enables. When you do have it, it changes the frontiers of what you can reasonably accomplish in an application.

2018: LiveView

It's 2018. We've had multiple minor point releases under our belt. Two Phoenix books have been printed, and the community is growing and happy.

I was thrilled with Channels. They're a fantastic abstraction for scalable client-heavy applications, where the server is mostly responsible for sending data and handling client events. They're still the Phoenix go-to for JavaScript and native applications.

But I knew I wasn't all the way there yet. Phoenix still relied on SPA libraries for real-time UI. But I'd moved on from that architecture, and internalized a new way of building applications in Elixir.

Think about how a normal web application works. Everything is stateless. When a user does something, you fetch the world, munge it into some format, and shoot it over the wire to the client. Then you throw all that state away. You do it all over and over again with the next request, hundreds or thousands of times per second. It worked this way in PHP and Perl::Mason, and it still works this way in modern frameworks like Rails.

But Elixir can do stateful applications. It allows you to model the entire user visit as a cheap bit of concurrency on the server, and to talk directly to the browser. We can update the UI as things happen, and those updates can come from anywhere - the user, the server, or even some other server in our cluster.

You don't want to build applications in Elixir the way you would in other frameworks.

I had a glimpse of how stateful UI applications worked on the client-side with React.js. We borrowed that React programming model and moved it to our Elixir servers.

With React, any state change triggers a re-render, followed by an efficient patch of the browser DOM. LiveView is the same, except the server re-renders the template and holds the state. Where React templates annotate the DOM with client events, such as <button onClick={this.clicked()}, in LiveView it's RPC events, like <button phx-click="clicked"> . With React, you're still context switching and gluing things with HTTP and serializers. Not with LiveView. Interactive features are friction-free.

Phoenix screams on Fly.io.

Phoenix is a win anywhere. But Fly.io was practically born to run it. With super-clean built-in private networking for clustering and global edge deployment, LiveView apps feel like native apps anywhere in the world.

Deploy your Phoenix app in minutes.  

This, again, alters the frontiers of what you can reasonably do in an application. Elixir applications don't need to arrange ensembles of libraries and transfer formats and lifecycles to build dynamically updating UI; dynamic server rendering is built in. Once you acclimate to having serverside-stateful UI, you look at your applications differently. There's no SPA complexity tax to pay for making features dynamic, and so whatever makes sense to be dynamic gets to be dynamic.

Still, at this point, we had only a worse-is-better approach compared to SPA alternatives. Any tiny change re-rendered the entire template. The server not only re-computed templates, but re-sent redundant data, usually just to patch a tiny part of the page. To make LiveView the first tool developers picked up off the shelf, we needed it to scale better.

2019-2020: LiveEEx

Part of the magic of React is that it's a powerful abstraction with an efficient implementation. When a small piece of the state underlying a React app changes, React constrains the update, and diffs it into place, minimizing changes to the DOM.

Through José Valim's work on the LiveEEx engine, Phoenix LiveView templates became diff-aware. They extract static and dynamic data from the template at compile-time. We compute a minimal diff to send to the client of only the dynamic data that has changed.

The abstraction doesn't change. You write the same code you did before. But now we send a payload better optimized than the best hand-written JSON API. It sounds too good to be true. Let's run through a tiny template to understand how this checks out.

Imagine we're building the interface for a thermostat with LiveView. We'll call it "Roost". Roost can display the current temperature, and you can bump it up and down with UI controls.

The first time you load the Roost interface it send a regular HTML response with your page. Next, a WebSocket connection is established, and LiveView sends an initial payload to the browser with the static and dynamic parts of the template split out:

Check out all its majesty

The browser now has a cache, not just of the template but of the state data that backs it. And the browser knows how to merge diffs from the server. UI updates are easy. LiveView's browser code zips the static values with the dynamics to produce a new string of HTML for the template. It hands that string off to morphdom to efficiently patch only those areas of the DOM that have changed.

Say a houseguest wants it warmer. They click the <button phx-click="inc">+</button> element. This wires up to an RPC function on the server which only increments @val:

def handle_event("inc", _, socket) do
  {:noreply, update(socket, :val, &(&1 + 1))
end

The server propagates the update. But not much changed! Up above, you might have noticed that we delivered the current temperature (a dynamic state variable) under the 2 key in our initial state. That's all we need to update; we just send {2: 70}to browsers. Here's what happens:

Wars have been fought over encoding formats for application state updates. Here we've sidestepped the controversy. A variable changes, we recompute linked templates, and the result goes on the wire.

Now, someone has left a window open, and the temperature is changing. The server notices, and lets clients know about it:

Pubsub.broadcast(MyApp.PubSub, "device:#{id}", {:set_temp, val})

Each LiveView process picks up the broadcast:

def handle_info({:set_temp, new_val}, socket) do
  {:noreply, assign(socket, :val, new_val)}
end

Everyone's browser receives another tiny {2: 68} payload, regardless of which server they are connected to. We also don't execute other template code like <%= strftime(@time, "%r") %>, because we know it didn't change.

Of course, it's not just numbers. Let's build an online dictionary, and let's give it autocomplete. Bear with me, this is neat.

Here's a simple HTML form:

defmodule DemoWeb.SearchLive do
  use DemoWeb, :live_view

  def render(assigns) do
    ~H"""
    <.form phx-change="suggest" phx-submit="search">
      <input type="text" name="q" value="{@query}" list="matches">
      <datalist id="matches">
        <%= for match <- @matches do %>
          <option value={match}><%= match %></option>
        <% end %>
      </datalist>
      <pre><%= @result %></pre>
    </.form>
    """
  end

When a user types something, we fire the suggest event, along with the contents of the text field. Here's a handler:

  def handle_event("suggest", %{"q" => q}, socket) when byte_size(q) <= 100 do
    {words, _} = System.cmd("grep", ~w"^#{q}.* -m 5 /usr/share/dict/words")
    {:noreply, assign(socket, matches: String.split(words, "\n"))}
  end

We update matches for this connection, which updates the datalist in the template. What falls out of the diffing engine is only the new strings! And just as importantly, we don't think about how that happens or make arrangements in advance; it just works.

Here's a problem that's familiar to lots of people who've built dynamic server rendered applications. You want to provide CSS feedback when users click a button. But the server can, at any point, be sending the client unrelated updates. Those in-flight update wipes out the CSS loading states you wanted for feedback. It looks like the server handled the click, but because of the race condition, the UI is lying. This leads to confusing and broken UX.

The Phoenix team solved this by annotating the DOM with phx-ref attributes that we attach to client diff payloads. These refs indicate that certain areas of the DOM are locked until that specific message ref is acknowledged by the server. All of this happens automatically for the user.

Features like this aren't glamorous, but they're the difference between a proof of concept for dynamic real-time applications and an actual, working application.

2021: LiveView Uploads, Redirects, and HEEx

First, we shipped binary uploads. Uploads use the existing LiveView WebSocket connection. You get trivial interactive file uploads, with file progress and previews. Once again, you don't have to think about how to make it work; it just does.

We also landed on a solution that allows you to perform direct-to-cloud uploads with the same code. You can start simple and upload direct-to-server, then shift to cloud uploads when that makes sense. Let's see it in action:

The code that makes this happen is shockingly small. Above, we have file progress, file metadata validation, interactive file selection/pruning, error messages, drag and drop, and previews. This takes just 60 lines.

You don't have to understand all that code, but I want to call attention to this:

  def mount(_params, session, socket) do
    {:ok,
     socket
     |> assign(:uploaded_files, [])
     |> allow_upload(:avatar, accept: ~w(.jpeg .png), max_entries: 3)}
  end

Notice how the declarative allow_upload API allows you to specify what uploads you want to allow, then the template defined in render/1 is reactive to any updates as they happen.

Uploading over WebSockets is neat. We're absolutely certain that the file landed on the same Elixir server our visitor initially randomly load-balanced to. This allows the LiveView process (your code) to do any post-processing with the local file right there on the same box. You can watch a live coding upload deep-dive here.

We also shipped a live_redirect and live_patch feature which allows you to navigate via pushState on the client without page reloads over the existing WebSocket connection. This might look like pjax or turbolinks. But it's not. Navigation happens over WebSockets. There's no extra HTTP handshake, no parsing authentication headers, no refetching the world. The UX is faster and cleaner than an SPA framework, latency is reduced, and it's easier to build.

You may have also noticed the ~H Elixir sigil in the previous examples. This is HEEx, and it's new. It's an HTML-aware template syntax, including validation at compile-time. Like React JSX, components can be expressed as custom tags. It looks like this:

<div class="profile">
  <Avatar.icon for={@user} width="100" height="100"/>
  <%= for suggestion <- @suggested_users do %>
    <.follow_button target={@suggestion} />
  <% end %>
</div>

The HEEx engine extends standard Elixir EEx templates with tag syntax, such as <Avatar.icon for={@user} … which internally compiles to a <%= component &Avatar.icon/1, for: @user %> macro call. It's structured markup and it composes nicely with HTML.

With the base HEEx engine in place, we foresee a budding Elixir ecosystem of off-the-shelf components for building out LiveView applications. Watch this space while we continue to extend our HEEx engine with features like slots and declarative assigns for compile-time checks.

The Future

We are excited to take LiveView, quite literally, around the globe. Fly.io makes it easy to deploy your LiveView processes close to users – and when LiveView is close to your users, interactions are immediate. We're already exploring optimistic UI on the server, where we can front-run database transactions with Channels, reflect the optimistic change on the client, and reconcile when database transactions propagate. Much of this is already possible today in LiveView with a little work, but with a turn-key global deployment under our feet, the Phoenix team can really dig in and explore making these kinds of previously unheard of ideas the status quo for day to day LiveView applications. Stay tuned!