LiveView

framework

LiveView

Definition

LiveView is a Phoenix framework feature that enables rich, real-time user interfaces rendered entirely on the server and delivered over WebSocket connections. Unlike traditional Single Page Applications (SPAs) that run JavaScript frameworks like React or Vue in the browser, LiveView maintains all UI state on the server as an Elixir process, sending minimal DOM diffs to the client for efficient updates. This architecture eliminates the need for separate frontend and backend codebases while providing a responsive, interactive experience that rivals client-side frameworks.

The core insight behind LiveView is that modern network latencies (typically 10-50ms for most users) are fast enough that server-rendered updates feel instantaneous for the vast majority of UI interactions. By keeping state on the server, LiveView eliminates entire categories of frontend engineering problems: state synchronization between client and server, API versioning, client-side caching invalidation, and the security implications of running business logic in the browser. The trade-off is a persistent WebSocket connection per user session and slightly higher server memory usage compared to stateless HTTP request handling.

LiveView was created by Chris McCord (also the creator of Phoenix) and first released in 2019. It has since evolved into a comprehensive framework for building interactive web applications, with features including live navigation (SPA-like page transitions without full page reloads), file uploads with real-time progress, and a component system that supports both stateless function components and stateful LiveComponents with their own lifecycle.

How LiveView Works

Understanding LiveView's internal mechanics is essential for building performant applications and debugging unexpected behavior.

The Connection Lifecycle

When a user navigates to a LiveView route, two separate renders occur:

  1. Static HTML render (HTTP): The initial page load renders the LiveView as static HTML on the server and sends it to the browser. This ensures the page is visible immediately, is SEO-friendly, and works even before the WebSocket connects.

  2. Live render (WebSocket): Once the page loads, the browser's LiveView JavaScript client establishes a WebSocket connection. The server spawns a dedicated GenServer process for this connection and performs a second mount, this time with the connected? check returning true. From this point forward, all interactions happen over the WebSocket.

defmodule PrismaticWeb.PerimeterDashboardLive do
  use PrismaticWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    if connected?(socket) do
      Phoenix.PubSub.subscribe(Prismatic.PubSub, "perimeter:updates")
      Process.send_after(self(), :refresh_metrics, 30_000)
    end

    {:ok,
     socket
     |> assign(:assets, PrismaticPerimeter.list_assets())
     |> assign(:security_rating, PrismaticPerimeter.current_rating())
     |> assign(:page_title, "Perimeter Dashboard")}
  end

  @impl true
  def handle_event("scan_domain", %{"domain" => domain}, socket) do
    case PrismaticPerimeter.initiate_scan(domain) do
      {:ok, _scan} ->
        {:noreply, put_flash(socket, :info, "Scan initiated for #{domain}")}
      {:error, reason} ->
        {:noreply, put_flash(socket, :error, "Scan failed: #{reason}")}
    end
  end

  @impl true
  def handle_info(:refresh_metrics, socket) do
    Process.send_after(self(), :refresh_metrics, 30_000)
    {:noreply, assign(socket, :security_rating, PrismaticPerimeter.current_rating())}
  end

  @impl true
  def handle_info(%{type: :asset_discovered} = event, socket) do
    {:noreply, update(socket, :assets, fn assets -> [event.asset | assets] end)}
  end
end

DOM Diffing and Patching

When server-side state changes (via handle_event, handle_info, or handle_params), LiveView re-renders the template, computes the difference between the previous and current rendered output, and sends only the changed portions to the browser:

Rendering PhaseWhat Happens
State Changeassign/2 or update/2 modifies the socket's assigns
Re-renderLiveView calls render/1, which evaluates the HEEx template
Diff ComputationLiveView compares static and dynamic parts of the template output
TransportOnly changed dynamic parts are sent as a compact JSON payload
Client PatchThe JavaScript client applies DOM patches using morphdom

The key optimization is that HEEx templates separate static HTML (sent once) from dynamic content (tracked per-change). A template with 1,000 lines of HTML but one dynamic value will only transmit the changed value, not the surrounding markup.

Architecture

Browser <-- WebSocket --> Phoenix.Endpoint --> LiveView Process (GenServer)
                                                      |
                                                      v
                                              render/1 (HEEx template)
                                                      |
                                                      v
                                              DOM diff --> Browser patch

Each LiveView connection spawns a dedicated server-side process (GenServer) that:

  1. Manages the view's state (assigns)
  2. Handles user events (clicks, form submissions, keyboard input)
  3. Subscribes to PubSub topics for real-time updates from other processes
  4. Re-renders and sends DOM diffs on state changes
  5. Monitors connection health and cleans up on disconnect

Because each LiveView is a BEAM process, it benefits from all of BEAM's properties: fault isolation (one user's crashed view does not affect others), preemptive scheduling (no single view can block others), and per-process garbage collection (GC pauses are bounded per-view).

LiveComponents

LiveView provides two component models for organizing UI code:

Function Components (Stateless)

Function components are pure functions that receive assigns and return HEEx markup. They have no lifecycle, no state, and no process -- they are simply reusable template fragments.

defmodule PrismaticWeb.Components.SecurityBadge do
  use Phoenix.Component

  attr :grade, :atom, required: true
  attr :score, :integer, required: true

  def security_badge(assigns) do
    ~H"""
    <span class={"inline-flex items-center rounded-full px-3 py-1 text-sm font-medium #{grade_color(@grade)}"}>
      <%= @grade %> (<%= @score %>)
    </span>
    """
  end

  defp grade_color(:A), do: "bg-green-100 text-green-800"
  defp grade_color(:B), do: "bg-blue-100 text-blue-800"
  defp grade_color(:C), do: "bg-yellow-100 text-yellow-800"
  defp grade_color(grade) when grade in [:D, :F], do: "bg-red-100 text-red-800"
end

Stateful LiveComponents

Stateful LiveComponents have their own state, lifecycle callbacks (update/2, handle_event/3), and can be targeted by events independently from the parent LiveView:

defmodule PrismaticWeb.Components.AssetTableLive do
  use PrismaticWeb, :live_component

  @impl true
  def update(assigns, socket) do
    {:ok,
     socket
     |> assign(assigns)
     |> assign(:sort_by, :risk_score)
     |> assign(:sort_order, :desc)}
  end

  @impl true
  def handle_event("sort", %{"column" => column}, socket) do
    column = String.to_existing_atom(column)
    order = if socket.assigns.sort_by == column, do: toggle_order(socket.assigns.sort_order), else: :asc
    sorted = Enum.sort_by(socket.assigns.assets, &Map.get(&1, column), order)
    {:noreply, assign(socket, sort_by: column, sort_order: order, assets: sorted)}
  end
end

HEEx Templates and Hooks

HEEx (HTML + EEx) is LiveView's template engine, designed specifically for server-rendered real-time UIs. It provides compile-time validation of HTML structure, component calls, and attribute types:

~H"""
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
  <.card :for={asset <- @assets} class="p-4">
    <h3 class="text-lg font-semibold"><%= asset.domain %></h3>
    <.security_badge grade={asset.grade} score={asset.score} />
    <button phx-click="scan" phx-value-id={asset.id} class="mt-2 btn-primary">
      Rescan
    </button>
  </.card>
</div>
"""

JavaScript Hooks

For interactions that require client-side behavior (drag-and-drop, third-party JavaScript libraries, clipboard access), LiveView provides a Hooks API:

let Hooks = {};
Hooks.ClipboardCopy = {
  mounted() {
    this.el.addEventListener("click", () => {
      const text = this.el.dataset.copyValue;
      navigator.clipboard.writeText(text);
      this.pushEvent("copied", { value: text });
    });
  }
};

Context in Prismatic

LiveView powers all of the Prismatic Platform's real-time dashboards. The platform enforces TailwindCSS-first styling with Flowbite components for all LiveView templates, explicitly forbidding inline styles and custom CSS.

Dashboard Routes

RouteLiveView ModuleDescription
/DashboardLiveMain platform dashboard with system overview
/perimeterPerimeterLiveEASM overview with security ratings
/perimeter/assetsPerimeterAssetsLiveAsset inventory with filtering and sorting
/perimeter/compliancePerimeterComplianceLiveNIS2/ZKB compliance assessment
/perimeter/easmPerimeterEASMLiveAdvanced EASM dashboard with drill-down

Real-Time Update Patterns

The Prismatic Platform uses three patterns for pushing updates to LiveView dashboards:

PatternMechanismUse Case
PubSub broadcastPhoenix.PubSub.broadcast/3Multi-user updates (new asset discovered, alert triggered)
Process.send_afterScheduled self-messagesPeriodic metric refresh (every 30s)
handle_info callbackDirect process messagingAgent status updates, scan completion notifications

Performance Considerations

LiveView's server-side rendering model introduces specific performance characteristics:

ConsiderationGuidance
Assign minimizationOnly store data needed for rendering in socket assigns
Temporary assignsUse temporary_assigns for large lists that need not persist in memory
Stream APIUse stream/3 for efficiently managing large collections
DebouncingUse phx-debounce on form inputs to reduce server roundtrips
Component granularityUse LiveComponents to scope re-renders to changed sections
PubSub selectivitySubscribe only to topics the current view needs
# Streams: efficient append/prepend without keeping full list in memory
def mount(_params, _session, socket) do
  {:ok, stream(socket, :assets, PrismaticPerimeter.list_assets())}
end
  • Phoenix - Web framework that LiveView is built into
  • GenServer - Behavior underlying each LiveView process
  • BEAM - Virtual machine providing process isolation for LiveView
  • WebSocket - Transport protocol for LiveView connections
  • PubSub - Distributed messaging for real-time LiveView updates
  • Channel - Alternative real-time communication layer in Phoenix
  • Flowbite - Component library used with LiveView in Prismatic
  • TailwindCSS - Utility CSS framework for LiveView templates
  • EASM - External attack surface module with LiveView dashboards
  • Process Isolation - Crash containment per LiveView connection
  • Plug - Middleware layer that routes requests to LiveView
  • Endpoint - Phoenix entry point managing WebSocket upgrades

See Also


Connect & Contribute

Created by Tomáš Korcak (korczis) | Open Source under GHL