View Source LiveCache (live_cache v0.3.0)

Briefly cache LiveView assigns to prevent recalculating them during connected mounts.

For example:

  • Cache a database query to avoid duplicate identical queries, resulting in a faster connected mount
  • Cache a random value to determine which content to display in an A/B test

By using assign_cached/4, an assign evaluated during the disconnected mount of a LiveView is temporarily cached in ETS, for retrieval during the connected mount that immediately follows.

def mount(_params, _session, socket) do
  socket = assign_cached(socket, :users, fn ->
    Accounts.list_users()
  end)

  {:ok, socket}
end

Cached values are not stored for very long. The cache is invalidated as soon as the connected mount occurs, or after 5 seconds (configurable), whichever comes first. In the event of a cache miss, the function is evaluated again.

Live navigation to the LiveView will always result in a cache miss.

The caching of LiveComponent assigns is not currently supported.

Scoping Cached Values

For assigns that depend on external parameters, the :scope option can be used to guarantee uniqueness of the stored value.

def mount(%{"id" => id}, _session, socket) do
  socket = assign_cached(socket, :post, fn -> Blog.get_post(id) end, scope: id)
  {:ok, socket}
end

Implementation Details

The cache is rehydrated by storing a one-time key in a <meta> tag in the DOM, which is then passed as a connection param when the LiveSocket client connects. For enhanced security, the cached values can also be scoped to the current session with the LiveCache.PerSession plug.

The cache is stored locally in ETS, and is not distributed. If your production application has multiple nodes running behind a load balancer, the load balancer must be configured with "sticky sessions" so that subsequent requests from the same user are handled by the same node.

Installation

Add live_cache to your list of dependencies in mix.exs:

def deps do
  [
    {:live_cache, "~> 0.2.0"}
  ]
end

In my_app_web.ex, update the live_view definition.

defmodule MyAppWeb do
  def live_view do
    quote do
      # [...]
      import LiveCache
      on_mount LiveCache.LiveView
    end
  end
end

In the root template root.html.heex, add a meta tag to the <head>:

<%= if assigns[:live_cache_key] do
  <meta name="live-cache-key" content={@live_cache_key} />
<% end %>

In app.js, modify the LiveSocket client constructor to include the value from the meta tag:

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content");
let liveCacheKey = document.querySelector("meta[name='live-cache-key']").getAttribute("content");
let liveSocket = new LiveSocket("/live", Socket, {
  params: { _csrf_token: csrfToken, live_cache_key: liveCacheKey },
});

Finally, add the LiveCache.PerSession plug to the router. This step is optional but highly recommended, as it ensures that cached values can only be retrieved from the session in which they were stored.

defmodule MyAppWeb.Router do
  use MyAppWeb, :router

  pipeline :browser do
    # [...]
    plug LiveCache.PerSession
  end
end

Configuration

The default config is below.

config :live_cache,
  ttl: :timer.seconds(5),          # Cache expiration time, in milliseconds; set to 0 to disable caching
  sweep_every: :timer.seconds(1)   # How frequently the cache is purged

Summary

Functions

Put an assign value, populating it from the cache if available.

Put a new assign value, populating it from the cache if available.

Functions

Link to this function

assign_cached(socket, key, fun, opts \\ [])

View Source
@spec assign_cached(Phoenix.LiveView.Socket.t(), atom(), (() -> any()), keyword()) ::
  Phoenix.LiveView.Socket.t()

Put an assign value, populating it from the cache if available.

On a cache hit during a connected mount, the cached value is used.

On a cache miss during a connected or disconnected mount, the result of fun is used. During the disconnected mount, this value is stored in the cache with an expiration.

Options

  • :scope - unique conditions to associate with this assign

Examples

def mount(_params, _session, socket) do
  socket = assign_cached(socket, :address, fn ->
    Accounts.get_address(socket.assigns.current_user)
  end)

  {:ok, socket}
end

def handle_params(%{"order_by" => _order_by} = params, _uri, socket) do
  # Only cache the orders list for a specific set of query params
  socket = assign_cached(socket, :orders, fn -> Orders.list_orders(params) end, scope: params)
  {:noreply, socket}
end
Link to this function

assign_cached_new(socket, key, fun, opts \\ [])

View Source
@spec assign_cached_new(Phoenix.LiveView.Socket.t(), atom(), (() -> any()), keyword()) ::
  Phoenix.LiveView.Socket.t()

Put a new assign value, populating it from the cache if available.

This function is identical to assign_cached/4, except it falls back to Phoenix.Component.assign_new/3 on a cache miss.

In other words, the order of priority for evaluating the assign is:

  1. Try to fetch from the cache.
  2. Try to fetch from existing assigns.
  3. Evaluate the anonymous function.