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 most sluggish of applications into a lightning fast one. On this page we explain some ways to do that.
To optimize the speed of a Bubble app, 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.
Advanced filtering is a powerful feature in Bubble, but it can cause massive slowdowns for your users if you're not careful. As of January 2020 Bubble performs advanced filtering operations client-side, which means they are done on the user's browser instead of Bubble's servers. 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 example, we need to find all of the users whose email address contains a certain text, which in this case is pre-defined as "airdev". If we do this using advanced filters, the application will download a batch of users into the browser, check if they match this condition, and download another batch if the matching users don't fill up the repeating group. Depending on how many users you have, that could take a long time - in this case on an app with 900 users, it downloaded around 300 users onto the page, 4mb of data, and took 20 seconds longer than it normally would to load the page.
An easier solution is to assign a yes/no field on the User object like "AirDev email", and then include that as a criteria in the search expression.
If your app has advanced filters and you can't figure out how to remove them, consider adding more fields directly to the object that you are filtering for. This takes a bit of work and is more advanced - you need to add new fields to that object, update the app logic to save new data to these new fields, and set up an API workflow that will calculate or update that field's value for all of your existing app data.
In the long run, this will be worth it due to the speed boosts for your users. Advanced filtering is rarely scalable if the list that must be filtered will eventually contain hundreds or thousands of items.
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 one below, where you find all messages that belong to threads that are linked to a project. However, this nested search (search inside a search) will 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).
We can't overstate the importance of solid privacy rules in your application. The primary reason is to keep your data secure. A side effect is that this may speed 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 this as a performance safety net - even if you mess up somewhere else and set up a search expression to download more items than it needs to, the search's results may be restricted due to privacy rules. This is mostly relevant if you're doing some operation on these items or if they're displayed in a repeating group.
More information on privacy rules can be found here.
Sometimes you may need to have really long workflows, with lots of actions, like the one below:
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.
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.
What's wrong with this search?
The issue there is that
Ignore empty constraints is checked and the
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:
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.
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.
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.
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.
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.
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:
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:
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.
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.
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:
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.
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.
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.
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:
Switch to the Network tab.
Load the page, you'll see something like the below screenshot.
Sort by Size, so that the biggest things are at the top (see screenshot 1 below).
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.
Expand the data shown in the Preview tab until you see what data is being shown (see screenshot 2 below).
When your page is loading thousands of different HTML nodes in order to display the page or some data view, it can dramatically increase loading times.
This is especially an issue when using repeating groups that can potentially contain large amounts of data (100+). To illustrate this: if these repeating groups render 20 HTML elements in each tab (a reasonable number for the Bubble engine), in order to display 50 data points you'll be rendering (showing) 1000 HTML elements on the page.
These numbers are OK, but will result in a significant amount of time to load, especially if the user's device isn't very fast.
When element rending can really get out of hand is using reusable elements inside of repeating groups. If that repeating group has a popup or menu inside of it, that group will be rendered in every single tab of the repeating group. For a data table with 3-4 different options that all require their own popup, some of which with several inputs, this could easily blow the number of elements rendered per tab out of the water (10 times as many elements, or more).
To avoid this build pattern, you'll need to do one of the following:
Determine if any of those popups or menus are unnecessary, in which case they should be removed from the reusable element; OR
Put those popups directly on the page where the dashboard is found. You can trigger them from the reusable element via custom states on the reusable elements and "Do when condition is true" workflows on the target page.
Open the target page
Paste the following text into the Console when the page is completely loaded:
For reference, here are some measurements atken in this way:
Number of HTML elements
Default owner's portal (Dashboard)
Default owner's portal (Users table, 26 users loaded)
airdev.co (AirDev landing page)
Google home page (google.com)
Facebook home page (logged in user, facebook.com)
Twitter landing page (not logged in, twitter.com)
If you load one part of your page and then hide it, those elements are also counted in the number of HTML elements.
If you are experiencing issues with speed in your Bubble editor rather than or in addition to speed issues on user-facing pages, you may have too many elements on one page. This can be resolved by converting groups on that page to reusable elements - the Bubble elements inside of the reusables are not rendered inside of the Bubble editor, decreasing the number of things that need to be loaded in order to make changes to a page.
After creating a reusable element, you may need to make changes to the logic for the element, if one of the following is true:
Elements inside the reusable were referring to other elements on the page (outside of the reusable).
Workflows inside the reusable were referring to elements on the page (that are now outside of the. reusable).
Some sort of "Do when condition is true" workflow was set up using elements inside the reusable.
Elements or workflows outside of the reusable were referring to elements that are now inside the reusable.
Another way to improve performance is to hide elements on the page when it is first loaded. The initial Bubble rendering will include less elements and the users will see the first content on the page sooner, as rendering each additional element takes time.
A good example of when to do this is when you're building a page that has some sort of internal Tab navigation. When a page is loaded, only the navigation menu, header, and footer are loaded, and the page loads the content of the tab afterwards. It may seem more natural to set some view to be the default view on a page and to have it visible on page load, but this adds a flash of unneccessary content and increases page load times.
To find potential causes for slowness, it can be helpful to use a web browser's developer tools to examine what data is being downloaded to the page and when. Here's a page that shows how to do that with Chrome.
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.
Have you followed the above recommendations but are 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: