A lot of Bubble apps offer a dashboard for their users, typically displaying relevant numbers and statistics. Bubble makes it easy to calculate counts, sums and averages quickly and present them as text or in charts, making it a great platform for that type of projects.
Database lookups in Bubble apps remains synchronized with your database, ensuring that all elements on your dashboard page consistently display the most current, accurate data.
This is all pretty sweet and awesome – Bubble does all of this under the hood, meaning that an app that you build in mere couple of hours can actually be more up-to-date and performant than many enterprise-level SaaS apps.
Of course, anything that is searched for, counted and calculated come at the cost of server resources. As you may already know, Bubble calculates the consumption of these resources into a metric called workload (the concept is workload is usually abbreviated as WU or workload units). This is a number that reflects the work that the the Bubble server does to power your app, weighted across different types of activities (such as database searches and different kinds of workflow actions). In the case of a dashboard, not only do you need to ask the server about the search results in the first place, but Bubble automatically gets a packet of data from the server every time something changes in the database to keep your data fresh.
Let’s look at how the numbers can pile up.
How a dashboard can spend workload
This article zeroes in on dashboards primarily because they tend to be more resource-intensive, and it’s not necessarily immediately obvious to developers-in-training why that is the case. It might seem like you’re burdening the server more when running a workflow (it’s not called workflow for nothing!), however, a high number of database-related actions are surprisingly less taxing than merely loading data for display on the screen.
First, let’s go over some theory, and then smash some numbers into a table to put everything into perspective.
The basic setup
Let’s imagine that we’re working on a marketplace eCommerce store where users can publish and sell their own products easily (similar to a site like Gumroad). We’re including a dashboard that loads as as soon as a seller logs into their account.
We want this first version of our dashboard to show the following information:
- Number of product items sold
- Total sum of all product items sold
- Average value of products sold
- Number of new users
Additionally, we want to add some simple drill-downs to the dashboard to make it more flexible:
- Time period (i.e. last 30 days, last 7 days. last 24 hours)
- Filter by product (i.e. only show data for Product A)
All of this is pretty straightforward and can be solved with a neat and clever database setup and Do a search for expressions that use Bubble’s built-in calculation operators (such as :count, :sum and :average).
But this ain’t that kind of guide – what we want to know is how this all looks from a WU viewpoint.
We established earlier that this is the landing page for sellers – in other words, the dashboard loads every time a seller logs into their account. Our app is a big success, and 5,000 sellers start selling products within the first year.
Now that’s awesome. So let’s look at some numbers
When one seller logs in, their dashboard immediate loads. This means that when the page loads, Bubble runs four different database queries to show the numbers and statistics. In plain english, they are:
- Search for the number of products sold within a given period and count them
- Search for the number of products sold within a given period and calculate the total sum
- Search for the number of products sold within a given period and calculate the average value
- Search for the number of products sold within a given period, and count the number of unique users on these sales
We also have three different events that can cause the queries to run again (partly or in whole):
- The seller reloads the page (leading all queries to run again)
- The seller filters the sales by a different date or product (leading all queries to run again)
- Something changes in the database, such as a sale coming in (leading Bubble to send some additional data)
Let’s make some assumptions about how how often these numbers update. Keep in mind that we have 5,000 users and are looking at this from a monthly (30-day) perspective:
|Event||Number of times per user/day||Queries|
|Search filter changed||3||1,800,000|
|Sales come in while dashboard is open||5||3,000,000|
|Total number of queries||6,000,000|
Holy smokes, are you seeing what I’m seeing? We’re performing search queries six million times over the course of a month!
Now, it’s important to note here that these figures aren’t precise workload computations, but are merely intended to illustrate how a page that displays extensive data to a substantial user base can significantly elevate the volume of server queries.
What to do?
Optimizing our dashboard
Let’s agree what our goal is: we want to reduce the WU consumption of the page, without significantly compromising its features and user interface.
First, let’s go over some of the strategic decisions that were made in the design of our original dashboard.
What do we show on page load?
From the viewpoint of performance and WU, what we choose to show as soon as the page loads is a key decision. If we decide to show a dashboard performing a number of different searches as soon as a user opens the page, we’ll know that the page load itself will be consuming a certain amount of WU.
What can we do about this? Well, that depends on your app: do your users access the page first and foremost to see the statistics? Or might they have other priorities in mind? If the charts and numbers are all they want, then by all means, load it all immediately.
But what if they want to add/edit products? Manage yesterday’s orders? Search for customers and issue refunds? The point is not to discuss how you should prioritize, but to draw attention to the fact that your priority decisions can have a major effect on the performance and WU consumption of your app.
If, rather than be displayed immediately, your charts are one click away, it’s not unrealistic that this simple design decision can reduce the WU consumed by the charts by anything from 25-95%. After all, if we show everything on page load, you know for sure that 100% of your users will run those four queries every single time, but if we hide it behind a button click, that number can be drastically reduced.
It’s often helpful to draw thought experiments to the furthest end of one spectrum to illustrate its potential benefits. Let’s assume that you load no data on page load, but simply an option set-based menu:
Here, instead of showing all the data, we’re letting the user decide what to see. Would this be a good user interface? That call is yours to make, but one thing is certain – this approach would spend almost no WU at all on page load.
The sweet spot may be somewhere in between the extremes of showing all data on page load versus showing no data on page load.
What data are your sellers interested in?
Taking that point to a more zoomed-in level, we can also ask the question “what data are my sellers interested in?”. For example, I included an “average sales sum” on the example: does that have value for your sellers?
The best way to decide is of course to ask them, either directly or by use of statistics and A/B testing. Again, the point is not to convince you to drop useful data to reduce WU usage at the expense of useful features, but to ask whether you can skip one data point, or (as in the previous example) hide it behind an actual query from the seller (such as one more button click before the data is loaded).
Could our users pay for WU?
If you run an app with different plans, you can also consider hiding reports behind a more expensive one. For example, basic plans could grant access to simple sales statistics, while more complex reports could be reserved for premium tiers.
This way, you reduce the number of initial page load queries for a potentially big portion of your users and also allows those who require more extensive data to foot the bill for the extra workload units.
To evaluate our technical setup in this case, one simple question makes up a good starting point: does the data need to be real-time by default?
The dashboard we set up is 100% real-time. As soon as a sale comes in, the dashboard will reflect the changes. Not only does this mean we perform some heavy server lifting on page load, but Bubble automatically keeps it up to date.
To again illustrate a point with a semi-absurd example: let’s say that all 5,000 of your sellers went to lunch for one hour, leaving their dashboard page open: Bubble would faithfully keep running server queries for all 5,000 any time relevant database changes happened, eating up WU in the process.
In Norwegian, we call it “fyre for kråka”, or “heating your house for the crows” (yes, we care deeply about the cost of heating our houses here up in the cold north). Basically, if you leave your house for a week but left all your heaters running, you’d be warming your house merely for the benefit of the crows.
This is not a realistic example in most scenarios, but it shows how easy it is to waste resource that you may not even be aware of.
What could we do instead?
An alternative approach would be to provide pre-calculated, periodic data. Although real-time data might seem like the most accurate or appealing choice, these types of statistics actually often lack practical use. When working with averages, real-time data can even distort insights: for instance, an average daily sales figure doesn’t carry much weight if the business day is not yet over.
What does periodic data mean? It simply means that statistics are generated on set intervals, such as:
- Once per hour/day/week/month
- When the page loads, if the data is older than X
- Whenever the user clicks an update link
- Whenever data changes (such as a completed sale)
Think about it: in many cases, historical data is static: yesterday will never generate more sales than it already did, and it doesn’t make much sense to re-calculate that same data over and over again.
Even with more recent data, such as today’s sales, there’s really no need to update the dashboard unless new sales data has come in since it was last loaded. Updating the data whenever sales and refunds are completed may be more efficient than every time the page loads unless the frequency of sales is very high. It also means that dormant seller accounts (people watching their dashboards but failing to get sales) don’t waste resources generating statistics that simply return zero.
How would we structure this? The full technical setup is out of the scope for this article, but you would set up a separate data type to store the statistical data and simply load the numbers from there. Data can be updated using backend workflows triggered at specific intervals or events.
For example, you could have a datatype called Statistics, that contain the numbers for a given period/seller that saves and displays the numbers without having to perform complex queries.
Remember, you can always set up queries to get more advanced filtering in addition to pre-calculated ones, but you don’t need to always load it with the page.
Let the user tell you if they need more complex statistics, and you’ll reduce the page load WU spend if you hide it behind a few clicks.
Moving filtering to the client
The local device doesn’t quite match the AWS servers in computing power, but for simply filtering or re-calculating stuff it’ll get the job done – in many cases even faster than the server does (since there’s no need for the data to be transferred to and from it). How to set up client-side filtering is again outside of the scope of this article, but you can read more about it in my books.
Maintaining a clean database
A cluttered database will slow down your app and spend more WU, since you’re giving the server more work to do. While eCommerce sites typically need to retain their sales data, there may still be things in your database that you can optimize. For example, you may be creating shopping cart data that is later abandoned, and it may no longer be worth keeping around.
When working with statistics, also keep data weight in mind – working with heavier data types is more taxing on the server.
Thoughts on the optimization process
As I mention in my book ‘The Ultimate Guide to Bubble Performance’, working with performance optimization is a war of a thousand battles. There’s rarely one single initiative that takes the app from tortoise to rabbit, but a lot of small improvements can make a big difference.
In the case of our dashboard, while we might not engage in a literal thousand battles, it’s easy to see that the process of optimization is a combination of strategic and technical changes that together should markedly decrease the workload consumption.
Remember, this isn’t a step-by-step tutorial or best practice-guide – in line with the majority of my content, I try to cultivate a certain mindset and strategical approach towards designing your app. My aim is to empower you to make informed decisions that are applicable to a lot of different scenarios, and not exclusively the one we’re looking at here.
Whether you agree with a specific approach or not has value in its own right – it shows that you’ve invested thought into how you should balance the user experience and workload optimization of your app.
As always – the best product is the one that ships
App development is a game of weighing different qualities up against each other and finding the right balance of compromises. Keeping your WU down can reduce the cost of hosting your app, but don’t let it become your only priority. After all – WU is there to be spent.
You may find ways to optimize your car’s fuel or battery consumption, but you wouldn’t get far if you refuse to turn the engine on. The key is to strike a balance between efficiency and functionality.