Hi everyone,
Ahead of the October 1st workload unit migration, I wanted to share some common areas you can optimise your Bubble app, largely without changing functionality. These are the most common major issues we find when auditing existing apps, and fixing these issues alone has allowed us to save our clients $1000s a year. Every app is different, but you should be able to go through this (non-exhaustive) list to check you have no glaringly obvious issues.
I’ve selected this list as they are either highly costly in their own right, or more costly than an alternative but used frequently.
We’re offering FREE workload optimisation which you can check out here: Free workload optimisation | Not Quite Unicorns - we’ll go through your app, recommend improvements, and even implement them for you, all for free.
Fuzzy Search / Search & Autocorrect
There are a number of fuzzy search / search & auto correct plugins on the marketplace. In short, they make searches more tolerant to typos, allowing your users to find results more easily. However, this necessitates downloading all of the possible search results to the client, and then filtering them client side. What does that mean?
Suppose we have a search for Users, and we want to search for users who’s bio contains a specific input’s value. Using a fuzzy search, we have to return all of the users (even if the table is paginated), and then filter. This costs 0.015 WU per Thing returned from the database. So, searching a database of 10,000 users will cost 150 WU as a minimum - not including the actual size of the data returned.
Instead, there are a few approaches you can use to remedy this. First, if your use case permits it, remove use of the plugin element and instead at a search constraint on the relevant field using contains keyword(s). So, it might be Do a search for Users where bio contains keyword(s) Input’s value. This will return relevant results for your search term, whilst only returning the necessary data. If you need to search multiple fields, you can use ‘Any field’ as the field to search.
If you only want to search specific fields, I recommend adding a ‘searchField’ text field to the relevant data type, that will be used to search over (Do a search for Users where searchField contains keyword(s) Input’s value). You keep this in sync using a backend trigger. For instance, if we want the search field to track the name and the bio for a user, we’ll create a trigger that checks if the name or bio fields have been modified, and then update the search field with the new name and/or bio. So long as you’re searching for this data much more than you’re modifying it, this will be significantly cheaper.
Also, Typesense.
Live text input searches
In the same vein as the above, there are a few plugins that will return an input’s live text value. Normally, Bubble will only update an Input’s value when focus is removed, or after a couple of seconds. However, this means that a new search is run for every character you enter. If I search for Users using the term ‘george’ as a constraint, I’m going to be doing 6 searches, as my search has 6 characters and we run a new search each time the input is changed.
This does make searches appear to be faster, but puts a lot of burden on the client if you’re returning lots of data, and also costs much more WU. Alternative approaches are using the native input’s value as a constraint, or adding a ‘filter’ button that will Display list in a Repeating Group, rather than updating the list any time you modify any filter.
Recursive workflows and lists
Recursive workflows are now, for the most part, a thing of the past, except in a few specific use cases. On new WU plans, Schedule API workflow on a List runs faster and on larger lists than before. It can run about 50 workflows per second depending on the complexity of the workflow.
If you need a recursive workflow, there’s a few optimisations you can make.
Instead of passing a List of Things into each recursion and selecting item # 1/2/3/4/ etc based on the iteration, you can reduce the list of things each time. So, I’ll run my operations on List of Things:item #1, and reschedule the workflow passing List of Things:items from #2. This ensures that you’re not passing more data than you need to between workflows.
However, it can often be even cheaper to pass a list of texts rather than list of Things into a recursive workflow. When we pass a List of Things to a backend workflow, we do one search to find the list of things. However, when the backend workflow is run, we do another search for the List of Things that we just passed it, which is a bit daft. Instead, you can pass a list of unique IDs, and convert the relevant one back to a data type using Do a search for (unique ID):first item. I wouldn’t implement this optimisation unless it’s a really common / WU intensive workflow - the ‘hacky’ workaround comes at a cost of lower maintainability.
User triggers
User triggers can be triggered more often than you think. If you use an admin API key to call one of your own backend workflows, then a new user is effectively created. This will set off any User triggers that meet the condition. For example, if you have a ‘sign up’ trigger that runs when User before change is empty, then using an admin API key will cause this to run, which could cost lots of WU.
Avoid SSAs where possible
SSAs (server side actions) are plugin actions that run in a Lambda server. Unfortunately, these are marked up hugely by Bubble. They should not be used regularly unless absolutely essential. If you do need to use it regularly, consider getting the code and hosting your own serverless function that you can call via the API Connector. It’ll be much cheaper, and have less ‘warmup’ time (after not being used in the last 15 minutes). Claude 3.5 Sonnet will help you create your own custom code serverless functions really easily - something perfect for data heavy operations / mathematical calculations that would otherwise require recursion.
Storing images as base64 inside rich text
Some rich text editor plugins, when images are added, will convert them to base64 (essentially text) and save that in the text field. This is extremely inefficient. It means that whenever you want to search your database, or retrieve data, you have to download all of the image data. If you have products, and each product has a 1MB image in its description, then returning 100 products from the database will use 100MB of data! Instead, we should store images as URLs. When we do this, we return URLs from the database, and then we can seperately download the images only when we need them (only if we’re displaying them).
The easiest way to tell if this has happened to you is to load up one of your text fields that has an image, and see how it’s stored. If the text field is really long, or has a lot of seemingly random characters, you’ve probably stored images as base64 rather than image URLs. There’s no native Bubble way to convert existing base64 images to URLs, but you can do it using some custom code that uses the data API to scrape your Things, get the base64 images, convert and upload to Bubble file manager, and replaces the base64 code with image URLs. If you can’t do this, then you may have to just remove these images all together, as it’s going to cost you a lot of WU and it makes performance abysmal.
Using Lists instead of Searches
Using large list of things is less performant than searches. And, we’ve all at some point come to the realisation that ‘hey, this should’ve been a search, not a list!’. Bubble themselves does the same thing - did you know they store all user invoices on a List on the user’s account, rather than just searching for them? Bit of a problem when you’ve got hundreds!
The severity of this issue depends on the specifics, but do just keep an eye out. If your lists are regularly approaching 100 Things / items, then it might be worth considering whether it should really be a list on another data type, as opposed to a search.