Engineering

LiveView Performance: Sub-250ms Page Loads

Performance engineering techniques for Phoenix LiveView applications

Mar 11, 2026 Β· 9 min read Β· Prismatic Engineering

#The 250ms Budget

The Prismatic Platform enforces a strict performance budget: every page must load in under 250 milliseconds, with server-side rendering completing in under 100ms and LiveView mounts finishing in under 150ms. These are not aspirational targets – they are enforced by performance gates in CI.

Achieving these targets across 30+ LiveView pages requires systematic optimization at every layer.

#Mount Optimization

The mount/3 callback is the most critical performance path. Every millisecond spent here delays the initial page render. The key principles:

Do the minimum in mount, defer the rest:

def mount(_params, session, socket) do
  socket =
    socket
    |> assign_defaults(session)
    |> assign(:loading, true)
    |> assign(:data, [])

  if connected?(socket) do
    send(self(), :load_data)
  end

  {:ok, socket}
end

def handle_info(:load_data, socket) do
  data = fetch_data()
  {:noreply, assign(socket, data: data, loading: false)}
end

This pattern renders a loading skeleton on the initial static render, then populates data over the WebSocket. Users see content in two phases but the perceived load time drops significantly.

Use try/rescue for production resilience:

def mount(params, session, socket) do
  {:ok, do_mount(params, session, socket)}
rescue
  e in [Ecto.QueryError, ArgumentError] ->
    Logger.error("Mount failed: #{Exception.message(e)}")
    {:ok, assign(socket, :error, "Failed to load page")}
end

Never let a mount crash take down the LiveView process. Catch specific exceptions and render an error state instead.

#ETS Caching for Render Data

Data that changes infrequently (navigation items, feature flags, configuration) should be cached in ETS rather than fetched on every mount:

defmodule PrismaticWeb.Cache.NavItems do
  @table :nav_items_cache
  @ttl_ms :timer.minutes(5)

  def get_nav_items do
    case :ets.lookup(@table, :items) do
      [{:items, items, inserted_at}] ->
        if System.monotonic_time(:millisecond) - inserted_at < @ttl_ms do
          items
        else
          refresh_and_return()
        end
      [] ->
        refresh_and_return()
    end
  end
end

This eliminates database queries for data that changes once every few minutes.

#Lazy Loading Patterns

Large datasets should never be loaded all at once. The platform uses three lazy loading strategies:

#Paginated Loading

def handle_event("load-more", _params, socket) do
  page = socket.assigns.page + 1
  new_items = fetch_page(page, socket.assigns.per_page)
  {:noreply, assign(socket, items: socket.assigns.items ++ new_items, page: page)}
end

#Tab-Based Loading

Only load data for the active tab. When the user switches tabs, fetch that tab’s data:

def handle_event("switch-tab", %{"tab" => tab}, socket) do
  data = load_tab_data(tab)
  {:noreply, assign(socket, active_tab: tab, tab_data: data)}
end

#Viewport-Based Loading

Use a JavaScript hook to detect when elements enter the viewport and trigger server-side data loading via pushEvent.

#WebSocket Optimization

LiveView communicates over WebSockets. Every assign/2 call that changes a value triggers a diff to be sent to the client. Minimize unnecessary assigns:

# Avoid: assigns unchanged data on every tick
def handle_info(:tick, socket) do
  {:noreply, assign(socket, data: fetch_data(), timestamp: DateTime.utc_now())}
end

# Better: only assign if data actually changed
def handle_info(:tick, socket) do
  new_data = fetch_data()
  if new_data != socket.assigns.data do
    {:noreply, assign(socket, data: new_data, timestamp: DateTime.utc_now())}
  else
    {:noreply, socket}
  end
end

#Measuring Performance

The platform uses :telemetry to measure mount times and render durations:

:telemetry.execute(
  [:prismatic, :liveview, :mount],
  %{duration: duration_ms},
  %{view: __MODULE__, action: :mount}
)

These measurements feed into performance dashboards and CI gates that reject deployments exceeding the 250ms budget.

#Results

With these patterns applied consistently, the Prismatic Platform achieves:

MetricTargetActual
Page load< 250ms~180ms average
Server render< 100ms~65ms average
LiveView mount< 150ms~90ms average
WebSocket reconnect< 500ms~300ms average
Browse all β†’