Canvas Performance: Speeding Up Searches and Page Loads
Practical techniques for making Canvas Bubble apps faster. Database search optimisation, page weight reduction, repeating group patterns, and when to denormalise.
Canvas (Airdev's Bubble framework) apps slow down for predictable reasons. Once you've learned what to look for, most performance problems have known fixes that can take a page from 8 seconds to under 2 without leaving Bubble.
This article covers the optimisations we use most often when speeding up Canvas apps at AppSavvy. It assumes you've measured first (use Bubble's step-by-step debugger for queries; Chrome DevTools throttled to "Slow 3G" for page loads).
If your app is genuinely past the point where optimisation helps - usually around the Bubble performance ceiling - it's worth considering whether a migration off the platform is the better investment.
The four big wins
In our experience, four optimisations account for 80% of the speed wins we get in Canvas apps. Most apps benefit from at least two of them.
1. Move slow searches off the critical path
A "critical path" search is one that has to complete before the user sees anything useful. Page-load searches, in particular, are critical-path.
Three ways to take a slow search off the critical path:
- Move it after the initial render. Show the page skeleton, then load the data via a workflow that fires after the page loads. The user sees the page in 800ms instead of waiting 4 seconds.
- Pre-compute and cache. If the search is expensive but the result doesn't change often, store the result on a record and read it directly. Recompute via a scheduled workflow or on the relevant change event.
- Pre-filter at the data layer. Make sure your privacy rules and constraints are reducing the result set before the workflow runs - not after. Searches that pull 50,000 records and then filter to 50 are doing 1000x more work than they need to.
2. Reduce page weight
Bubble pages get heavy because of plugins, complex element trees, and reusable elements that are themselves heavy. Aim for under 2MB initial JavaScript on a key page.
Practical reductions:
- Audit plugin inventory. Each plugin adds JavaScript to every page. Plugins used on one page should be loaded conditionally, not globally.
- Lazy-load reusables. Reusable elements you use in modals or below-the-fold sections don't have to load with the page. Use Bubble's conditional element rendering to defer.
- Reduce repeating group complexity. Each row of a repeating group renders all its child elements. A repeating group of 50 rows with 20 elements per row renders 1000 elements at page load.
3. Denormalise hot reads
For data that's read often but changes rarely, denormalisation is often the right tradeoff.
Examples:
- Storing the user's display name on every Booking, so showing a booking list doesn't require joining to User
- Storing a count on the parent (e.g.
Order.line_item_countupdated whenever line items change) - Storing a "last activity" timestamp denormalised onto the entity it represents
The cost is that you have to maintain the denormalised data when the source changes. The benefit is that hot reads become instant.
4. Cache via scheduled workflows
For data that's expensive to compute but only needs to be fresh every few minutes (dashboards, aggregations, reports), pre-compute the result via a scheduled workflow and store it.
The page reads the cached result. The expensive computation happens once every 5-15 minutes, not on every page load.
This trades freshness for speed. Most dashboards don't actually need real-time data.
Searching efficiently
Bubble's search behaviour is the source of most performance problems. A few rules:
Use indexed fields in searches
Bubble doesn't expose database indexes directly. But certain field types are effectively indexed - the unique ID field, the Created Date, the Modified Date, and field types that Bubble has internally optimised for searching.
Whenever possible, structure your searches to filter on these indexed fields first, then on application-level fields.
Avoid "search by reference" chains
A search like "all Bookings whose User's Organization's Plan is 'Premium'" requires three joins. Each join multiplies the work.
If you find yourself doing reference-chain searches often, denormalise. Store the user's plan directly on the Booking (or compute it via a workflow when relevant) so the search becomes flat.
Avoid :filtered after the search
Bubble's :filtered operator runs in JavaScript on the client, after the data has been fetched from the server. That means Search for Bookings:filtered fetches every booking and then filters.
If you can express the filter as a search constraint, do it. :filtered is slow and consumes WU on the records you're throwing away.
Use "First Item" instead of "Count > 0"
If you only need to know whether a record exists, "First Item is not empty" is faster than "Count > 0" because it short-circuits.
This sounds minor. On a hot path that fires 100 times per page, it isn't.
Page load patterns
A few page-load patterns we use to keep things fast:
Render the skeleton first
The page renders immediately with placeholder UI (skeleton screens, "Loading..." text, blurred boxes). The real data arrives shortly after.
This is mostly a perceived-performance win, but it's significant - 200ms to first paint feels 2-3x faster than 2 seconds to fully-loaded.
Single-column above the fold
The first thing the user sees should load in under a second. That usually means avoiding above-the-fold content that depends on slow searches or external plugins.
Push complex content below the fold or behind tabs.
Lazy-load images
Bubble has built-in lazy loading for images via the image element's settings. Use it. An image-heavy page can be 5MB if everything loads eagerly.
Use the Bubble CDN for static assets
Files uploaded to Bubble are served via Bubble's CDN. If you're hosting images elsewhere (e.g. your own S3) and not via a CDN, the page is waiting on a slower origin.
Workflow patterns
A few workflow patterns that pay off:
Update in batches, not one record at a time
If a workflow needs to update 100 records, don't loop through them with one "make changes to thing" per record. Use "make changes to a list of things" which is significantly faster and consumes less WU.
Use API workflows for anything over 5 seconds
Page workflows that run on the client. The user is staring at the screen while they execute. If the workflow takes more than 5 seconds, the user perceives the app as broken.
Move long workflows to API workflows that run on the server, scheduled (often via "Schedule API workflow"). The page workflow just initiates the job and returns. The user gets immediate feedback while the work happens in the background.
This is the most-effective single optimisation for write-heavy pages.
Avoid frequent custom event firing in repeating group rows
Custom events that fire per-row in a large repeating group multiply the workflow work. If a 100-row group fires "row hovered" on every mouse move, the workflow runs hundreds of times.
Throttle, debounce, or move the work to a single event at the group level.
When optimisation isn't enough
Some patterns won't yield to optimisation:
- You've hit the data scaling ceiling. Tables over 500k records start performing poorly regardless of how you search them. The ceiling is real.
- You need real-time updates. Bubble's polling-based updates aren't designed for real-time. Plugins help but add their own cost.
- You need multi-region or fast global performance. Bubble's hosting is concentrated in specific regions; users on the other side of the world pay a latency tax.
If any of these is the bottleneck and matters to your business, optimisation buys time but doesn't solve the problem. The right answer is usually a migration to a modern code stack - either now or planned for the next year.
What to do next
If you'd like an external review of your app's performance, request a free Bubble app audit - the Performance section measures the same metrics we'd use internally.
If you want a senior pair of eyes on specific performance bottlenecks, book a 30-minute discovery call. We do optimisation engagements (typically 2-6 weeks) as a standalone service, separate from migration.
Read next: The Canvas tech debt audit and Canvas plugin strategy.
Got a Bubble or Canvas app you’d like a second pair of eyes on?
30-minute discovery call. We’ll look at your app live and tell you honestly what we’d do next.
Or grab the Bubble migration playbook PDF.