https://www.phoenixframework.org/blog/phoenix-liveview-1.0-released?release=1.0 Phoenix Framework [phoenix-or] [icon] Open main menu Docs Community Source Blog Phoenix Framework [phoenix-or] Close menu Docs Community Source Blog Phoenix LiveView 1.0.0 is here! Posted on December 3rd, 2024 by Chris McCord --------------------------------------------------------------------- LiveView 1.0.0 is out! This 1.0 milestone comes six years after the first LiveView commit. [commits] Why LiveView I started LiveView to scratch an itch. I wanted to create dynamic server-rendered applications without writing JavaScript. I was tired of the inevitable ballooning complexity that it brings. Think realtime form validations, updating the quantity in a shopping cart, or real-time streaming updates. Why does it require moving mountains to solve in a traditional stack? We write the HTTP glue or GraphQL schemas and resolvers, then we figure out which validation logic needs shared or dup'd. It goes on and on from there - how do we get localization information to the client? What data serializers do we need? How do we wire up WebSockets and IPC back to our code? Is our js bundle getting too large? I guess it's time to start turning the Webpack or Parcel knobs. Wait Vite is a thing now? Or I guess Bun configuration is what we want? We've all felt this pain. The idea was, what if we removed these problems entirely? HTTP can go away, and the server can handle all the rendering and dynamic update concerns. It felt like a heavy approach, but I knew Elixir and Phoenix was perfectly suited for it. Six years later this programming model still feels like cheating. Everything is super fast. Payloads are tiny. Latency is best-in-class. Not only do you write less code, there's simply less to think about when writing features. Real-time foundations unlock superpowers Interesting things happen when you give every user and UI a real-time, bidirectional foundation as a matter of course. You suddenly have superpowers. You almost don't notice it. Being freed from all the mundane concerns of typical full-stack development lets you focus on just shipping features. And with Elixir, you start shipping features that other platforms can't even conceive as possible. Want to ship real-time server logs to the js console in development? No problem! What about supporting production hot code upgrades where browsers can auto re-render anytime CSS stylesheets, images, or templates change - without losing state or dropping connections? Sure! Or maybe you have an app deployed planet-wide where you do work across the cluster and aggregate the results in real-time back to the UI. Would you believe the entire LiveView, including the template markup and RPC calls, is 350 LOC? These are the kinds of applications that LiveView enables. It feels incredible to ship these kinds of things, but it took a while to arrive here for good reasons. There was a lot to solve to make this programming model truly great. How it started Conceptually, what I really wanted is something like what we do in React - change some state, our template re-renders automatically, and the UI updates. But instead of a bit of UI running on the client, what if we ran it on the server? The LiveView could look like this: defmodule ThermoLive do def render(assigns) do ~H"""
Temperature: {@thermostat.temperature}
Mode: {@thermostat.mode}
Temperature: {format_unit(@temperature)}
""" At compile time, we convert the template into a struct like this: %Phoenix.LiveView.Rendered{ static: ["Temperature:", "
"] dynamic: fn assigns -> [ if changed?(assigns, :mode), do: assigns.mode, if changed?(assigns, :temperature), do: format_unit(assigns.temperature) ] end } We know the static parts never change, so they are split from the dynamic Elixir expressions. Next, we compile each expression with change tracking based on the variables accessed within each expression. On render, we compare the previous template values with the new and only execute the template expression if the value has changed. Instead of sending the entire template down on change, we can send the client all the static and dynamic parts on mount. After mount we only send the partial diff of dynamic values for each update. To see how this works, we can imagine the following payload being sent on mount for the template above: { s: ["Temperature: ", "
"], 0: "cooling", 1: "68" } The client receives a map of static values in the s key, and dynamic values keyed by their index in the statics. For the client to render the full template string, it only needs to zips the static list with the dynamic values. For example: ["Temperature: ", "68", "
"].join("") "Temperature: 68
" With the client holding a static/dynamic cache, optimizing network updates is no work at all. Any server render following mount simply returns the new dynamic values at their known index. Unchanged dynamic values and statics are ignored entirely. If a LiveView runs assign(socket, :temperature, 70), the render/1 function is invoked, and the following payload gets sent down the wire: {1: "70"} Thats it! To update the UI, the client simply merges this object with its static/dynamic cache: { { s: ["Temperature: ", "
"], 0: "cooling", 1: "70F" => 1: "70" } } Then the data is zipped together on the client to produce the full HTML of the UI. Of course innerHTML updates blow away UI state and are expensive to perform. So like any client-side framework, we compute minimal DOM diffs to efficiently update the DOM. In fact, we've had folks migrate from React to Phoenix LiveView because LiveView client rendering was faster what their React app could offer. Optimizations continued from there. Including fingerprinting, for comprehensions, tree sharing, and more. You can read all about each optimization on the Dashbit blog. We apply these optimizations automatically and for free thanks to our stateful client and server connection. Most other server rendered HTML solutions send the whole fragment on every update or require users to fine tune updates by hand. Best in class latency We've seen how LiveView payloads are smaller than the best hand-written JSON API or GraphQL query, but it's even better than that. Every LiveView holds a connection to the server so page navigation happens via live navigation. TLS handshakes, current user auth, etc happen a single time for the lifetime of the user's visit. This allows page navigation to happen via a single WebSocket frame, and fewer database queries for any client action. The result is fewer round trips from the client, and simply less work done by the server. This provides less latency for the end-user compared to an SPA fetching data or sending mutations up to a server. Holding a stateful connections comes at the cost of server memory, but it's far cheaper than folks expect. At a baseline, a given channel connection consumes 40kb of memory. This gives a 1GB server a theoretical ceiling of ~25,000 concurrent LiveViews. Of course the more state you store, the more memory you consume, but you only hold onto the state you need. We also have stream primitives for handling large collections without impacting memory. Elixir and the Erlang VM were designed for this. Scaling a stateful system to millions of concurrent users isn't theoretical - we do it all the time. See WhatsApp, Discord, or our own benchmarks as examples. With the programming model optimized on both client and server, we expanded into higher level building blocks that take advantage of our unique diffing engine. Reusable Components with HEEx Change tracking and minimal diffs were ground-breaking features, but our HTML templates still lacked composability. The best we could offer is "partial"-like template rendering where a function could encapsulate some partial template content. This works, but it composes poorly and is mismatched in the way we write markup. Fortunately Marlus Saraiva from the Surface project spearheaded development of an HTML-aware component system and contributed back to the LiveView project. With HEEx components, we have a declarative component system, HTML validation, and compile-time checking of component attributes and slots. HEEx components are just annotated functions. They look like this: @doc """ Renders a button. ## Examples <.button>Send! <.button phx-click="go">Send! """ attr :type, :string, default: nil attr :rest, :global, include: ~w(disabled form name value) slot :inner_block, required: true def button(assigns) do ~H""" """ end An invalid call to a component, such as <.button click="bad"> produces a compile-time warning: warning: undefined attribute "click" for component AppWeb.CoreComponents.button/1 lib/app_web/live/page_live.ex:123: (file) Slots allows the component to accept arbitrary content from a caller. This allows components to be much more extensible by the caller without creating a bunch of bespoke partial templates to handle every scenario. Streamlined HEEx syntax When we introduced HEEx and function components, we added a new syntax for interpolating values within tag attributes along with :if and :for conveniences for conditionally generating templates. It looked like this: