Optimize for speed

Some best practices on how to make your Bubble app perform at its best.
You're deep in developing your brand new Bubble app and start testing it, only to find out that some parts of it are running painfully slow. You're cursing Bubble under your breath and/or increasing your app's capacity (and your Bubble bill) by 10x.
We've all been there and the answer is usually refactoring/optimizing your application, which can often turn the sluggishest of the sluggish applications into a lightning fast one. Below are some ways that you can do that.
To optimize how fast Bubble works, you have to think like Bubble. That means that you have to try to understand what operations Bubble is doing in order to achieve your desired result.

Minimize/eliminate advanced filters

Advanced filtering is a very powerful feature in Bubble but one should be avoided if at all possible. The reason is that Bubble performs advanced filtering operations "client-side", which means they are done on the user's browser. To do that, the browser has to first download all of the records that need to be filtered and then filter them. Let's look at an example
In this absurd example, we need to find all of the users whose email address is between 13 and 26 characters long. If we do it using advanced filters, the application will first need to download all of the users to the browser and then look at each one's email address to figure out which ones match the criteria. Depending on how many users you have, that could take a long time. An easier solution is just to have a field on the User object that's "Number of characters in email", and then include that as a criteria in the search expression.

Minimize/eliminate nested searches

Let's imagine a data structure where there is a single Project that has multiple Message Threads, each of which has multiple Messages. If you want to find all messages within a particular project, you may want to do a search like the below, where you find all messages that belong to threads that are linked to a project. However, this nested search is likely to take longer because Bubble first has to find all of the threads that belong to a project and then find all of the messages that belong to one of those threads.
Alternatively, you could link each Message object directly to the Project object, which seems redundant (as you can always reference Message's Message Thread's Project) but also allows you to make your search more direct and thus faster.
Coincidentally, this technique also makes setting up Privacy Rules easier (as Bubble is a bit restrictive when it comes to the types of rules that you can set up).

Make sure your privacy rules are set up

We can't overstate the importance of solid privacy rules in your application. The obvious reason is keeping your data secure but the less obvious but still important one is speeding up your app. If you have tight privacy rules, the amount of data that will ever be sent to your browser is always limited by those privacy rules. Think of it as a safety net - even if you mess up somewhere else and ask a search expression to download more items that it needs to, it won't be able to do so due to privacy rules. More information on privacy rules can be found here.

Move lengthy actions to API workflows

Sometimes you may need to have really long workflows, with lots of actions, like the one below:
This might take a while
Such a workflow may take a while, especially if it involves things like external API calls, making changes to/copying a list of things, or complex searches. An easy solution is moving a part of the workflow to be an API workflow instead. That way you just need to execute the part that has to happen right on the page (charging the user in this case) and then pass whatever parameters are needed to the API workflow that lives on the server side.
This would be much quicker
The other benefit of this approach is that it makes things more modular - you can now call that same API workflow from a different part of the application without having to rebuild its logic.

Don't accidentally ignore empty constraints

What's wrong with this search?
BAD (potentially)
The issue there is that Ignore empty constraints is checked and the email field is set to the value of Group email, which means that if Group email is ever empty, the search will ignore the email constraint and will return ALL of the users in the database. There are a few ways to remedy this:
  1. 1.
    Don't use Ignore empty constraints. This is the easiest but is also sometimes bad advice because this feature can keep your search expressions cleaner. For example, if you have a repeating group that you want to show all of the users and a search box on top of that repeating group that the user can enter a keyword in to filter the repeating group, you can use the Ignore empty constraints option to get away with using a single search expression for the repeating group.
  2. 2.
    Make sure that the constraint is never empty. This is harder to do than it seems - in the example above, we can set Group email's value When page is loaded but, if you do that, there will be a split second before the page is loaded, when the group is empty. And during that time your repeating group may try to load all of the Users into it, which will slow the entire application down.
  3. 3.
    Conditionally set the search expression only if the constraint value isn't empty, which ensures that the search is not performed until it's ready to be performed.

Short lists are faster than searches

Let's imagine that your database has an object called Project and an object called Invoice. Each project can have multiple invoices. The default database structure would be to create a Project field on each Invoice object and then Do a search for Invoices where Project is equal x. However, if you're looking to speed up your application a bit, you may want to create a List of invoices on each Project and refer to that list directly instead of doing a search.

But long lists are slower

The above only applies if the list is relatively small. Let's imagine a different scenario - you have an object called Song and an object called Tag. Each song can have multiple tags (classical, rock, etc.) applied to it. In this case we wouldn't want to store a List of songs on each Tag because that list may get very large and Bubble struggles working with large lists. Instead, we should have a field called List of tags on each Song and always perform a search for songs if we're looking to access songs that have a particular tag.

Add an abstraction for searches in Repeating Groups

Any time you put something into a Repeating Group, you should keep in mind that whatever you put in there will happen multiple times, once per each row of the RG. Let's imagine that we are building a messenger and want to display a list of threads along with the number of unread messages in each thread (that is messages that are unread by the current user). Here's how we may structure that:
A search will happen in each cell
The issue with the above is that in every single cell there will be a search that will occur. So if you have 100 threads, there will be 100 searches that will need to happen in order to display the correct number of unread messages on each line. A different way to do it is as follows:
Add a Repeating Group somewhere on the page (can be hidden) which searches for all of the relevant messages that are unread by the user, like this:
Step 1: This RG will search for all of the relevant unread messages once.
In each cell of the Message Thread repeating group, add an expression that filters the Messages repeating group down to only the messages that belong to the Thread in each cell of the RG.
Step 2: This RG will not perform any more searches but instead just filter the first RG.
As the result, the search for unread messages now only has to happen (and be sent from the server to the client) once, at which point it's just filtered directly on the page, which is faster.

Data API can be your friend

Sometimes you can use Bubble's Data API to do things faster. Let's imagine the following scenario.
We are building a reminder app where, upon signup, each user has 30 Reminder objects created for them. There are 3 ways we can do that:
  1. 1.
    Create all of the objects right on the page. This approach will be messy and slow - you'll have to create objects one-by-one and also you'll have unnecessary workflows on the page.
  2. 2.
    Create all of the objects through Schedule API workflow on a list action. This approach will be much cleaner but the downside is that it will be both and asynchronous, meaning that you won't easily be able to know when the creation process is done and you also won't be able to refer to the list of objects created.
  3. 3.
    Use the bulk create functionality of the Data API. This approach is the cleanest, fastest, synchronous, and will return the full list of objects created.

Get familiar with the network tab of Chrome

You can use the Network tab of Chrome's Developer Tools to see how your page is loading and what's taking up the most time/space. You can read more about that here but, in the meantime, here's a quick guide to seeing big searches:
  1. 1.
    Open the Developer Tools console in Chrome by going to View -> Developer -> JavaScript console.
  2. 2.
    Switch to the Network tab.
  3. 3.
    Load the page, you'll see something like the below screenshot.
  4. 4.
    Sort by Size, so that the biggest things are at the top (see screenshot 1 below).
  5. 5.
    If you see items called "search" or "msearch" at the top, those represent searches that the application is doing. Click on one to inspect it.
  6. 6.
    Expand the data shown in the Preview tab until you see what data is being shown (see screenshot 2 below).
Screenshot 1: List of largest operations on your page
Screenshot 2: What's getting downloaded with a single search operation

Boost capacity as the last resort

If you've done everything possible and you're still getting warnings like the below, it may be worthwhile to add some capacity to your application. You can always experiment using Bubble's "boost capacity" feature first before paying for it.

Handle delays gracefully

You've optimized the optimezeable and you've boosted the boosteable but still experiencing delays because of something that just needs a long time to execute? Make your users' experience more pleasant by letting them know what's going on or keeping them entertained while they're waiting:
Our loading popup for the Algo tool