matt murtaugh.
about. skills. work. uses. blog.

In with the new, out with the old. Migrating from Livewire to AlpineJS

Last year my business partner mentioned that we should use the extra 50" TVs as leaderboards to help gamify our recently expanded selection of events at Divrsion. This is easier said than done, but I loved the idea.

The idea was that players would check in to events with a consistent login. Their account would hold a history of events that they attended. Players would accumulate points based on how they do in tournaments.

We already heavily used Challonge, so we needed not reinvent the wheel. We could use our system for player registration and import players into Challonge via their API. I didn't want to do the complex calculations that Swiss tournaments require.


The initial version of the app, called Play @ Divrsion (or Play for short), was built using Laravel, Livewire, AlpineJS, and TailwindCSS (otherwise known as the TALL stack). It also used Pusher for communication between the dashboard and player check-in screens.

Once an event was marked as complete, the system would assign points based on position. The leaderboards would automatically refresh and display the new information.

The Leaderboards

While I wanted to give an overview of the Play app as a whole, the point of this post is to share the changes I made with version 2 of the app.

We built the leaderboard using Livewire to power the data and all the animations. This method proved to be a straightforward way to get up and running quickly.

When a leaderboard was loaded, it would also determine the next leaderboard through a few additional DB queries.

The screens should rotate through different leaderboards located at different URLs. For example:

  1. leaderboard.test/overall

  2. leaderboard.test/overall/84c36f19-9eb1-4274-8fa2-8ed858fbbe1c (for a specific game)

Once it reached the end of the list, it would go back to #1 and rinse and repeat. Below is a screenshot from the original version of the leaderboard.

The leaderboard themselves worked, but they relied on heavy DB queries and would constantly hit the DB (every 15s all day, every day). It made the server work much harder than needed, much more often than required. Every couple of days, it would hit an error on the server and break one or both displays, requiring a reboot of the Raspberry Pi.

But like the first version of many things, optimization wasn't a priority. Making sure everything worked was the main task.

The Refactor (rewrite)

Having used the system for nearly a year, I had fine-tuned what I wanted the app to do. I knew that the leaderboard didn't need to be so complicated. We relied heavily on wire:poll to rotate through different screens and refresh the content.

I had a few main goals for the rewrite:

  • Don't rely on Livewire when AlpineJS (or vanilla JS) would be sufficient.

  • Decouple the leaderboard code from the main app (communicating through an API)

  • Make the leaderboard more stable

  • Make the leaderboard feel more professional. Making animations and transitions feel more subtle and stable

The Results

I'm happy with how the new leaderboard turned out. I decoupled it from the Play app, and it now happily sits alongside the other displays and is powered by AlpineJS & Pusher instead of Livewire.

On the initial load, the leaderboard will pull all the current data it needs from a new API. The API compiles everything into a single call, and AlpineJS does the rest of the work. Instead of loading new pages for each "screen" like the old leaderboard, the new one loads it once and transitions through the content using setTimeout().

I can't say enough about how much of an improvement this is. The performance looks something like this:

Previously each page load was anywhere between 25-35 DB queries depending on the number of players:

  • On average, we would have 3 or 4 different displays rotating every 15 seconds.

  • The leaderboards are only updated once weekly

  • Since the screen would reload every 15 seconds, you could potentially have 140+ DB queries a minute, all day, every day.

The new leaderboard will only do a single API call when it is first loaded, using around 10 DB queries:

  • The leaderboard won't call the API again until a Pusher event requires it. AlpineJS handles moving from screen to screen instead of a new page load. It already has the data it needs, and it's already formatted.

Overall benefits look like this:

  • Using a Pusher event, I can tell the display that the content has changed. In the past, I would have to reset the Raspberry Pi on each board to force a refresh.

  • No more 404 errors that break the displays completely. The new system will gracefully handle errors from the API. I can fix those errors and tell the display to refresh without resetting the Raspberry Pi.

  • Because AlpineJS handles everything, I can utilize its built-in x-transtion to update the images, the players, and the game data between each screen. It makes the leaderboards feel much smoother and more professional.

© 2024 matt murtaugh. All rights reserved.