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 douse Cachex.Warmeralias BlogApp.Postsrequire Loggerrequire Jsonrs@cache_table :blog_app_cache@posts_cache_key {:posts, :list_posts}def interval, do: :timer.minutes(60)def execute(_args) dodata = [get_posts()]{:ok, data, [ttl: :timer.minutes(60)]}enddef get_cached_posts() doget_or_put(@posts_cache_key)enddefp get_posts() doposts = Posts.list_published_posts()posts =BlogAppWeb.Api.V1.PostsView.render("index.json", %{posts: posts})|> Jsonrs.encode!()|> :zlib.gzip(){@posts_cache_key, posts}enddefp get_or_put(key) docase Cachex.get(@cache_table, key) donil ->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)}")nilendvalue ->valueendendend
Implementation Steps
First, create a cache.ex
file in the lib folder:
defmodule BlogApp.Cache do@moduledoc """Cache"""@cache_table :blog_app_cacheimport Cachex.Specdef child_spec(_init_arg) do%{id: @cache_table,type: :supervisor,start:{Cachex, :start_link,[@cache_table,[warmers: [warmer(module: BlogApp.Cache.PostsWarmer, state: "")]]]}}endend
Next, add the warmer module to your application.ex
:
defmodule BlogApp.Application do@moduledoc falseuse Applicationdef start(_type, _args) dochildren = [# ... existing code ...{BlogApp.Cache, []}]Supervisor.start_link(children, strategy: :one_for_one, name: BlogApp.Supervisor)endend
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 douse Cachex.Warmeralias BlogApp.Postsrequire Loggerrequire Jsonrs@cache_table :blog_app_cache@posts_cache_key {:posts, :list_posts}def interval, do: :timer.minutes(60)def execute(_args) dodata = [get_posts()]{:ok, data, [ttl: :timer.minutes(60)]}enddef get_cached_posts() doget_or_put(@posts_cache_key)enddefp get_posts() doposts = Posts.list_published_posts()posts =BlogAppWeb.Api.V1.PostsView.render("index.json", %{posts: posts})|> Jsonrs.encode!()|> :zlib.gzip(){@posts_cache_key, posts}enddefp get_or_put(key) docase Cachex.get(@cache_table, key) donil ->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)}")nilendvalue ->valueendendend```elixirdefmodule BlogApp.Cache do@moduledoc """Cache"""@cache_table :blog_app_cacheimport Cachex.Specdef child_spec(_init_arg) do%{id: @cache_table,type: :supervisor,start:{Cachex, :start_link,[@cache_table,[warmers: [warmer(module: BlogApp.Cache.PostsWarmer, state: "")]]]}}endend```elixirdefmodule BlogApp.Application do@moduledoc falseuse Applicationdef start(_type, _args) dochildren = [# ... existing code ...{BlogApp.Cache, []}]Supervisor.start_link(children, strategy: :one_for_one, name: BlogApp.Supervisor)endend```elixirdef 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