#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)}
endThis 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")}
endNever 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
endThis 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:
| Metric | Target | Actual |
|---|---|---|
| Page load | < 250ms | ~180ms average |
| Server render | < 100ms | ~65ms average |
| LiveView mount | < 150ms | ~90ms average |
| WebSocket reconnect | < 500ms | ~300ms average |