WU Optimization
Best practices guide
Overview
Since Bubble's latest announcement regarding new pricing in April 2023, the community's focus has shifted from trying to consume less capacity to trying to consume fewer Workload Units (WUs).
Following our mission statement of setting the standard for no-code development, we have studied how the new system works and determined some best practices for developing an application in Bubble.
Best practices
We believe in building scalable, performant, robust, extensible, and secure applications. To achieve this, we will continue to build applications by adhering to proven standards and patterns that align with our objectives. Additionally, we will investigate and use solutions that follow these standards while consuming fewer WUs.
There is always a cost-effective solution available for each feature, which is the one we aim to use in our builds.
Mindset
Now, we not only prioritize performance but also consider the efficiency of WUs. Before performing any operation, we should ask ourselves if it could be simplified, if we truly need a certain piece of data, or if we have tested its WU consumption.
There are obvious conclusions like saying that using fewer actions will consume less WUs, so simplify is better (the simplest logic that solves a problem is usually the best one) but to be really sure we should always measure.
The :count example
Suppose you need to show an element conditionally when the Search:count is 0. It's a simple operation for the server, and Bubble documentation says that a :count operation consumes 0.20 WUs.
But if you take a look at your app's WU metrics, you'll see that the numbers are higher. This is caused by a Bubble bug. So, how should we do? We have to experiment and measure.
Let's examine this example in a Bubble app where we created three pages, each with only the following Search, and applied two different operators to it:
Page 1: Search (1,000 items):count is 0 -> 3.86 WU
Page 2: Search (1,000 items):first item is empty ->0.69 WU
Page 3: Search (1,000 items):count ->0.20 WU
The first comparison is buggy and download the first 10 items of the search to the page (it shouldn't).
Look at the image: it's making a Search (Overall -> Fetching Data -> Search)
So the total number of WUs will vary based on the objects weight.
WU = 3.86
The second comparison download just one item to the page.
Look at the image: it's making a Search (Overall -> Fetching Data -> Search)
WU = 0.69
The third operation is only the search and the :count operator. It only downloads the number of items, the count, in this case the number 1,000.
Look at the image: it's making an Aggregate (Overall -> Fetching Data -> Aggregate Search)
WU = 0.20
So the logical option here is to store the Search:count in a variable (group) and then make the comparison in the page. That comparison is going to cost you 0 WUs.
The conclusion is: experiment and measure. Don't take anything for granted.
The bug only happens with the comparison with the number 0, so other comparisons will only consume only 0.20 WUs, i.e.:
search:count is 1
search:count < 1
search:count > 1 search:count ≤ 1
search:count ≥ 1
search:count is 2
etc.
API Calls
API Calls consume WU based on the number of API Calls (0.1 WU) and the bytes you send and receive so:
Try to batch your API Calls when possible
Only ask for the info you really need, i.e.: if the service uses pagination just ask for one page and only ask for more pages when/if needed
Bubble keeps the API Calls results in cache. Don’t force the system to update the call if you don’t really need the updated results just to show the last updated data
Auto-binding fields
We’ve experimented with the consumption of a binding input versus modifying the same thing with a standard input and a button and a “Make changes to the thing” action. We did the modifications on some fields of the Current user
Performing an auto-binding operation in one Current User field consumes 1.05 WU
Modifying several fields in the Current User consumes 1.12 WU
Based on those numbers, saving more than two fields is cheaper using a button and a “Make changes to a thing…” action than using auto-binding fields. Therefore our recommendation is to avoid auto-binding fields except if there is only one field to modify (or if the UX absolutely needs the auto-binding feature), if you need to modify two or more fields, use a standard input and a button.
Client vs Server actions
Bubble has announced that implementing a workflow on a page is marginally cheaper than scheduling that same workflow. To optimize cost, we should utilize computations in the client browser by keeping workflows on the page.
However, we should prioritize reusability and user experience. In order to achieve this, we’ll move workflows to the backend if:
The workflow is going to be called from multiple places
The workflow is too slow on the page and negatively impacts UX
Current user
Bubble always downloads the Current User on each page, so it's important to keep that in mind. If there is a value related to each individual user that you will need frequently, consider storing it in the Current User. This can save you WUs because the data will already be accessible in the browser when needed.
Database design
We want to continue designing our databases to be scalable and perform well. When you are designing your database think about:
What data will you need to show in your application (RG):
If you have a Search page where you only need certain values of a data type, you could benefit from having a main Data type with fewer fields (only the ones you need on the Search page) and then link to another “satellite” Data type where you’ll store the data that it’ll be needed in, for example, a product Detail page.
Important: This is not considered a best practice in database design, and one-to-one relationships between tables (Data Types) are used sparingly. You could use this structure occasionally, for example when an object has a lot of data on it and only a little of it is needed for the search page.
Bubble has announced that they will think of improving how they send the search results to the page by sending only the fields you need.
This would reduce the amount of data sent to the page, and make the point above unnecessary.
But beware: that’s not yet in their public roadmap.
We continue to NOT recommend using lists to store things in a record when the expected number of items is higher than 30. So:
If the expected number of items is 30 or less, use a list.
Lists consume fewer WUs than searches.
Lists stored on a field are a great solution if we need to access just a few items without consuming too many WUs.
If you expect the number of items to exceed 30, avoid using a list. Although the advantage of not needing nested searches in certain cases may seem appealing, it's not worth it if the application's performance will suffer when it scales.
Instead, create a new Data Type and add a field to link to the main Data Type.
Data types scale better than lists on an object.
Searches
Each search result consumes 0.015 WU so:
Use constraints to reduce the number of results
Download only the necessary results when you need to display them.
Always pre-filter data as much as possible using constraints. Minimize the usage of advanced filters because it requires downloading all results to the page, which not only negatively impacts performance but also consumes more WUs.
Variables
We recommend using variables to store search results and other values on the page to facilitate debugging and maintenance. Using variables also helps avoid creating similar searches that can consume more WUs.
How and where to store variables
To store a variable you’ll create:
a Group if you need to store a thing, i.e. Search for invoices:first item
a Repeating Group if you need to store a list of things, i.e. Search for invoices
Preventing unnecessary searches
To guarantee the Search only executes when necessary, set the data source of the Group or RG in a conditional to be applied only when the data is required. For instance, if invoices will be displayed in a "Group List of Invoices" but the group is not visible at all times, link the data source to the group being visible.
Then you can reference that var - RG invoices’ List of invoices whenever you need them on the page without triggering a Search by default.
How to constrain our search to minimize WU consumption
The more constrained the Search, the less data we download to the page and the fewer WUs it consumes.
You should evaluate how to optimize your Search based on your requirements. A thorough analysis should consider the following factors:
How many results can this Search yield? Only a few items? Hundreds, maybe thousands?
If a few: download all and apply filters later.
If gathering hundreds or thousands of results: constrain the Search as much as possible initially and retrieve only when necessary at a later stage.
Examples:
For an RG that may potentially display hundreds or thousands of results, the optimal setup would involve using a variable with a default search configuration that is as constrained as possible. For instance, consider an app with an RG displaying invoices, with potentially thousands of items, the following steps can be helpful:
Create an Option set for the invoice status, values: “Pending” and “Paid”, then store this in a “var - invoice status” Group.
You’ll show only "Pending" invoices by default
The variable will store the search of invoices constrained by "Status = var-status".
When the user wants to see "Paid" ones, update the var-status to "Paid" with a “Display in the var - invoice status” action (being on the page it won’t consume WUs)
Using this configuration, we can start with a less expensive (more constrained) search and allow the user to request more results if needed later.
For an RG with just a few results, consider the following approach:
Store the whole Search in a variable.
Use the variable as the data source and filter results directly in your Repeating Group.
This setup incurs only one paid (WUs) Search, even though your user sorts, filters, or toggles the results on the page.
It's important to note that you should still perform this analysis and use the same approach even if you don't intend to use variables to store results and have the Search directly on your RG's data source.
Conditions
Bubble checks the conditions from left to right. If the first condition is not met, the subsequent conditions are not evaluated. Therefore, using a first condition that relies on an element in the page (e.g., if Group X is visible and...) can guarantee no resource-consuming actions occur unnecessarily.
Having a first condition that relies on an element in the page (i.e.: if Group X is visible and…) will ensure that no consuming actions take place when they aren’t necessary.
For example, let's say you want to display a group if the Search:first item’s Likes exceeds 50 and a specific toggle is enabled.
The condition should be written as follows:
Toggle Likes is checked and Search:first item's Likes > 50
By putting the lighter condition (e.g., checking the toggle) first, the Search won't be performed if the toggle isn't checked, saving resources.
Remember to exercise caution when ordering your conditions, and always prioritize the lightest ones first.
Workflows
Do each X seconds
Polling the database, modifying/creating records in the database, or any similar actions at regular intervals is not recommended. These actions can quickly add up WUs, compromising the application's performance and increasing your billing costs.
Do when condition is true
These actions are triggered only when the conditions change. Therefore, please follow the recommendations outlined in the Conditions section above. Additionally, remember to select "Just once" if that's applicable to your use case.
When the page is loaded
"When page is loaded" workflows will be triggered every time you change the URL, such as adding a parameter to the URL to navigate to another tab. If the workflow doesn't need to be fired upon changing tabs, consider using a variable within the page to track if the workflow has already executed. This will allow you to prevent the redundant initiation of workflows when you navigate to other sections within the same page.
Result of step X
Using the Result of step X will guarantee that the search action is not repeated.
Suppose you modify a User in one action and then require another action that affects the same user within the same workflow. In that case, make sure to reference the user using the Result of step X operator instead of using another search or method. This step is crucial because Bubble cannot guarantee that the search is not repeated, even if the search parameters are the same. Using the Result of step X operator ensures that you are referencing the same user and avoid consuming unnecessary WUs.
More info
Please review the official Bubble documentation to understand this new system thoroughly.
This document is a work in progress, and we will keep updating it as we, and the Bubble community, continue to investigate and discover new and better solutions. We also encourage everyone to experiment, measure, and share their findings with us. We would love to hear about your experiences related to optimization and appreciate any feedback or ideas you may have.
You can follow us in twitter to receive more tips about WU optimization and other best practices in Bubble development.