Quick Tip

Use caching to speed up data loading in Phoenix LiveView

cachex liveview cache

In this post, I want to show you how to leverage cache in Phoenix LiveView to avoid hitting the database twice when mounting a LiveView page. The reason is that the query might potentially be expensive and slow down the page with a bad user experience as a result.

I will do this by using Cachex, but the same principle applies to other libraries and also using pure ETS.

Install Cachex

Begin by adding Cachex to the dependencies.

# mix.exs
def deps do
  [
    {:cachex, "3.4.0"}
  ]
end

And run

mix deps.get

Next step is to start the Cache and can either start it manually where its directly called or like there, in the application.ex among the list of children. In this example, I use the name :data_cache

# lib/tutorial/application.ex
children = [
  # Other children
  {Cachex, name: :data_cache},
]

Load data with cache

With the latest version of LiveView, 0.17, you can now load data in an on_mount-hook. This is exactly what I want to do. Just because I can move som logic from the Live-component and also at a later stage, use the same cache logic for more than one resource.

`elixir # lib/tutorialweb/live/initassigns.ex defmodule TutorialWeb.Live.InitAssigns do import Phoenix.LiveView

alias Tutorial.Customers

def loadcustomers(params, _session, socket) do { :cont, socket |> assign(:customers, Customers.list_customers()) } end end `

And then I can call this in the on_mount hook in the Customers live view like

`elixir # lib/tutorialweb/live/customerlive/index.ex defmodule TutorialWeb.CustomerLive.Index do use TutorialWeb, :live_view

alias TutorialWeb.Live.InitAssigns

onmount {InitAssigns, :loadcustomers}

def mount(params, session, socket) do {:ok, socket} end # other code end `

With this change, I have now moved the loading of customers out from the LiveView into the on mount hook.

I want to achieve 3 things with the caching function:

  1. It needs to be reusable
  2. It should have a find or fetch functionality
  3. I should be able to set TTL (time to leave) since that can vary depending on situation
@cache_name :data_cache # same as in application.ex
  @default_ttl 1_000 * 60 * 10 # 10 mins
  
  def with_cache(key, fun, ttl \\ @default_ttl) do
    case Cachex.get(@cache_name, key) do
      {:ok, nil} ->
        result = fun.()
        Cachex.put(@cache_name, key, result, ttl: ttl)
        result
      {:ok, result} ->
        result
    end
  end

This means that if the data does not exist in cache, it will execute the passed in function and fetch the data from the database and then put the result in the cache with the given cache key and then return the result.

So, for my example, the final results look like this:

`elixir defmodule TutorialWeb.Live.InitAssigns do import Phoenix.LiveView

alias Tutorial.Customers

@cachename :datacache # same as in application.ex @defaultttl 1000 60 10 # 10 mins

def loadcustomers(params, _session, socket) do customers = withcache("list-customers-cache", (fn -> Customers.listcustomers() end)) { :cont, socket |> assign(:customers, customers) } end

def with_cache(key, fun) do case Cachex.get(@cache_name, key) do {:ok, nil} -> result = fun.() Cachex.put(@cachename, key, result, ttl: @defaultttl) result {:ok, result} -> result end end end `

NOTE that I wrap the list_customers in a function:

(fn -> Customers.list_customers() end)

Otherwise it would of course be executed right away.