Optimizing Elixir Phoenix Actions with Huge JSON Responses Using Cached, Gzipped Values

#elixir#phoenix#phoenixframework

Hello everyone,

I'm excited to be back and share some recent optimizations I've implemented for a client project. We've been developing a React Native mobile application that includes a wiki-like section providing users with step-by-step health improvement information.

The Challenge

One significant challenge we faced was handling a massive JSON response—approximately 20MB in size—retrieved from a single endpoint during the app's loading process. While splitting API requests and loading content on-demand was an option, we also wanted to ensure offline accessibility for users. This made a one-time load during the initial launch beneficial, despite taking around 6 seconds depending on internet speed.

The Solution

Looking for a quick optimization, particularly for the wiki endpoints, we found the perfect solution in the cachex library. This impressive tool includes a warmer module that automatically refreshes cached data either on boot or after a specified ttl (time-to-live).

By integrating cachex into our system, we've significantly improved the loading process, providing users with a smoother experience while maintaining offline accessibility. Let me walk you through how we implemented this caching solution for our Phoenix Controller actions with large JSON responses.

defmodule BlogApp.Cache.PostsWarmer do
  use Cachex.Warmer

  alias BlogApp.Posts

  require Logger
  require Jsonrs

  @cache_table :blog_app_cache
  @posts_cache_key {:posts, :list_posts}

  def interval, do: :timer.minutes(60)

  def execute(_args) do
    data = [
      get_posts()
    ]

    {:ok, data, [ttl: :timer.minutes(60)]}
  end

  def get_cached_posts() do
    get_or_put(@posts_cache_key)
  end

  defp get_posts() do
    posts = Posts.list_published_posts()

    posts =
      BlogAppWeb.Api.V1.PostsView.render("index.json", %{
        posts: posts
      })
      |> Jsonrs.encode!()
      |> :zlib.gzip()

    {@posts_cache_key, posts}
  end

  defp get_or_put(key) do
    case Cachex.get(@cache_table, key) do
      nil ->
        case key do
          @posts_cache_key ->
            {key, posts} = get_posts()

            Cachex.put(@cache_table, key, posts)
            Cachex.get(@cache_table, key)

          _ ->
            Logger.error("[your_app] cache key not found: #{inspect(key)}")

            nil
        end

      value ->
        value
    end
  end
end

Implementation Steps

First, create a cache.ex file in the lib folder:

defmodule BlogApp.Cache do
  @moduledoc """
  Cache
  """
  @cache_table :blog_app_cache

  import Cachex.Spec

  def child_spec(_init_arg) do
    %{
      id: @cache_table,
      type: :supervisor,
      start:
        {Cachex, :start_link,
         [
           @cache_table,
           [
             warmers: [
               warmer(module: BlogApp.Cache.PostsWarmer, state: "")
             ]
           ]
         ]}
    }
  end
end

Next, add the warmer module to your application.ex:

defmodule BlogApp.Application do
  @moduledoc false

  use Application

  def start(_type, _args) do
    children = [
      # ... existing code ...
      {BlogApp.Cache, []}
    ]

    Supervisor.start_link(children, strategy: :one_for_one, name: BlogApp.Supervisor)
  end
end

Finally, integrate the cached, gzipped data in your Phoenix controller:

def index(conn, _params) do
  {:ok, posts} = PostsWarmer.get_cached_posts()

  conn
  |> put_resp_header("Content-Encoding", "gzip")
  |> put_resp_content_type("application/json")
  |> send_resp(200, posts)
end

Results

This simple implementation provides a powerful speed boost. For processing large JSON files, we've also started using jsonrs, which further enhances performance.

The key benefits of this approach include:

  • Significantly reduced response times
  • Decreased server load
  • Improved user experience
  • Maintained offline functionality

Thank you for reading!


Available for consulting in Elixir, Go, JS, and Big Data. Visit our website: bitscorp.co Find me on GitHub: github.com/oivoodoo, github.com/bitscorp My other blog: https://dev.to/oivoodoo

defmodule BlogApp.Cache.PostsWarmer do
  use Cachex.Warmer

  alias BlogApp.Posts

  require Logger
  require Jsonrs

  @cache_table :blog_app_cache
  @posts_cache_key {:posts, :list_posts}

  def interval, do: :timer.minutes(60)

  def execute(_args) do
    data = [
      get_posts()
    ]

    {:ok, data, [ttl: :timer.minutes(60)]}
  end

  def get_cached_posts() do
    get_or_put(@posts_cache_key)
  end

  defp get_posts() do
    posts = Posts.list_published_posts()

    posts =
      BlogAppWeb.Api.V1.PostsView.render("index.json", %{
        posts: posts
      })
      |> Jsonrs.encode!()
      |> :zlib.gzip()

    {@posts_cache_key, posts}
  end

  defp get_or_put(key) do
    case Cachex.get(@cache_table, key) do
      nil ->
        case key do
          @posts_cache_key ->
            {key, posts} = get_posts()

            Cachex.put(@cache_table, key, posts)
            Cachex.get(@cache_table, key)

          _ ->
            Logger.error("[your_app] cache key not found: #{inspect(key)}")

            nil
        end

      value ->
        value
    end
  end
end

```elixir
defmodule BlogApp.Cache do
  @moduledoc """
  Cache
  """
  @cache_table :blog_app_cache

  import Cachex.Spec

  def child_spec(_init_arg) do
    %{
      id: @cache_table,
      type: :supervisor,
      start:
        {Cachex, :start_link,
         [
           @cache_table,
           [
             warmers: [
               warmer(module: BlogApp.Cache.PostsWarmer, state: "")
             ]
           ]
         ]}
    }
  end
end

```elixir
defmodule BlogApp.Application do
  @moduledoc false

  use Application

  def start(_type, _args) do
    children = [
      # ... existing code ...
      {BlogApp.Cache, []}
    ]

    Supervisor.start_link(children, strategy: :one_for_one, name: BlogApp.Supervisor)
  end
end

```elixir
def index(conn, _params) do
  {:ok, posts} = PostsWarmer.get_cached_posts()

  conn
  |> put_resp_header("Content-Encoding", "gzip")
  |> put_resp_content_type("application/json")
  |> send_resp(200, posts)
end
← Back to all posts

© Copyright 2023 Bitscorp