SPA - Architectural considerations - Large App - what to do?

Hey all,

We have been building an application for a long time and it is quite sizable.
In the beginning we received a lot of customer complaints about the loading speed moving from one page to another, so to catch this we created a Single Page App with a nice loader in the beginning. It takes about 15 - 20 seconds to get the page loaded but after that the application is very snappy.

We have 1000s of workflows and to keep things organized we create reusable groups for what normally would have been separate pages.

Lately we have been refactoring the application and found that the behavior of focus groups within the elements was causing a design limitation. We worked around this by tweaking popups to behave like focus groups but it comes with some limitations that sometimes are not ideal.

Solution ?

We are now thinking of converting all page reusable elements back into regular elements and putting them on the page. Apart from some workflows that may break - which we can deal with - we will loose the information structure on workflows that we have right (i.e. ringfenced per page/element)and are a bit afraid that the Bubble workflow editor will not really like to have 2000+ workflows in it and that Bubble editor will timeout and crash a lot .

I’m tempted to test it on a separate branch but I thought that since perhaps there are some best/good practices I might as well ask experienced Forum users what the best way to approach this would be.

Apart from Focus Groups and some non-DRY behavior in our app everything runs smoothly once the page is loaded (with the exception of some complex query results in tables which seem to time out on new results after they have been loaded initially - but that is a separate topic)

Thanks!

1 Like

Don’t do it

3 Likes

Yeah, don’t do it :rofl:

If the SPA is really huge, just move some stuff into separate pages. Prioritise the ones which are heavy but users don’t navigate between often.

If already in reusables, it should be fairly easy.

1 Like

Definitely do NOT do this. 2000+ front end WFs and 15-20 second load time strongly indicate that there are some significant issues witht he app design and many WFs should prob be moved back-end.

1 Like

Thanks for your responses. This is helpful!

Problem is the loading time between when switching between the pages. This is not just the case with the content and data heavy User sections, it also is the case when switching between public pages (e.g. from landing page to the terms page (latter only a handful of buttons a plain text container and resuable lightweight footer)

I have for example already tried getting rid of various plugins that load packages via the header (so packages are loaded even when they are not needed on a page at all - which would be a really nice feature within Bubble to untie packages from specific pages based on user selection - but that is a whole other thing :slight_smile: ) or I tried creating custom plugin alternatives to select .js packages stored within a closer proximity etc. Nevertheless this was not really helping me that much (used Lighthouse to explore the difference which was almost negligible but then again I didn’t test things like AirAlert which I expect to have practically no impact - then again who knows are these packages stored on the Arctics :wink: )

The 10-15 second loading for the app itself is not a problem for our users, we made some nice loading graphics and afterwards the app behavior is really snappy.

Using the app and fetching the data is where I would expect a bit more latency - especially with some more demanding queries - and cause some delays, due to the data traveling distance (request halfway around the globe and back > 100ms), while - if I understand it correctly - the actual app image may be stored somewhere in close proximity due to the cloudfront CDN) so switchhing pages should not take such a long time. But maybe I’m wrong..

Regarding the workflows, almost everything we CRUD is done in the backend, and all API calls are handled in the backend as well. Only exception is when we create a Thing client side that is needed in multiple API calls in one workflow (just for the sake of ease) but these cases are rare. Workflows we have are a lot of navigation patterns / do when a condition is true / reset when a value has changed etc. and many calls to the backend.
App has no images/files etc. all data.

Overall I’m very happy with how Bubble behaves, but indeed I’m a bit worried about what happens when would place all things on one page - But I get from your response that is a clear DON’T!! :slight_smile:

I’ll consider to create a development branch to see how te interaction will be when I put the reusable elements on seperate pages. Fortunately we do have all elements tied to a strict Options setup connected to parameter navigations and visibility triggers with page elements as optional values, but I’m expecting a 3 - 4 sec navigation lag for the first couple of page to load when a user navigates between things, which I think is not acceptable due to the high frequency of navigation and some users explaining about this when this was our original setup.

Not hindered by technical expertise, and thinking as I’m writing this, to tackle this navigation lag, is it perhaps possible to create some sort of load mechanism for specific pre-selected pages that, directly after the first payload on a specific page is rendered are loaded (e.g. user that bypasses the login will directly start downloading payload on the page X they are navigated into and as soon as the page renders the client continues loading pages Y and Z regardless of the user trigger a navigation that would otherwise trigger that page to load?

Just did this for the first 4 reusable pages/elements. The loading time (first paint) when switching is more than 5 seconds when switching and then the page loader (i.e. popup with loading svg) is still visible (from first paint until page loaded) for about 2 seconds.. and no I’m not on a legacy 56k2 modem :slight_smile:

Within the SPA everything after full load is instantly available and doesn’t break (so don’t fix it?).

Looks like I’m on a path creating huge new issues to unlock some small design gains.. though I agree that ideally it makes sense creating more pages but it is just much too slow.

Just did a performance recording in Chrome and passed the .json to LLM:

The single biggest takeaway from the trace

Your initial load cost is dominated by Bubble’s reactive engine initializing and stabilizing, not by data transfer or DOM size alone.

That means optimization must reduce ā€œthings evaluated at startupā€, not micro-optimize workflows.

What NOT to touch (this is important)

Based on the trace, do not waste time on:

  • Reducing workflow count just for the sake of it

  • Worrying about RAM usage

  • Plugin paranoia (no obvious plugin loop signature)

  • Network/data prefetch tricks (not your bottleneck)

These will not materially move the needle.

What WILL move the needle (in order of ROI)

1) Kill startup evaluation cascades

This is the #1 win.

Concrete rule:

No ā€œDo when condition is trueā€ should be able to fire during initial load unless it is absolutely required.

Practical steps:

  • Add a global app_ready = yes/no state

  • First thing on page load: do only what’s needed to show UI

  • Flip app_ready = yes

  • Gate everything else with app_ready = yes

This alone often cuts startup time by 30–50%.

  1. Ensure hidden views do zero data work

From experience + this trace:

  • Hidden views are still binding expressions

  • Many searches likely execute even if not visible

Action:

  • Default RG data source = empty

  • Apply the real search only when active_view = this_view

This reduces both CPU and GC pressure.

  1. Enforce ā€œsingle-instance UIā€ strictly

You already suspected this — the trace confirms it matters.

  • ONE modal host

  • ONE drawer host

  • ONE overlay system

Anything duplicated multiplies:

  • conditions

  • evaluations

  • GC churn


4) Replace watcher logic with explicit entry events

Wherever possible:

  • Stop ā€œDo when active_view = Xā€

  • Instead: router detects view change → triggers Enter X custom event once

This converts continuous evaluation into one-time execution.

Something to digest and convert I Guess. Performance take aways: for data load on a thing set a condition on every element that that only starts loading data once the element is visible (sounds like a lot of re-work and I always thought that data would only be fetched when a thing is visible.. :frowning: )

You can get 2.6 to 2.8 on huge SPA, for fcp.

so the issue is only at startup once everything is loaded things run stable and CPU is no issues anymore?

Yes — that is the correct conclusion, with one important nuance.

The accurate mental model

What your trace shows:

  • Startup (first 15–20s):

    • High CPU usage

    • Heavy JavaScript execution

    • Many reactive evaluations

    • GC churn

    • Some non-deterministic ordering
      → this is where all the pain lives

  • After stabilization:

    • CPU drops to normal

    • Navigation is snappy

    • Interactions are cheap

    • No sustained load pressure

So yes: once everything has settled, CPU is no longer a problem.


The nuance (this matters)

While things appear stable after startup, two caveats apply:

  1. If a ā€œDo whenā€¦ā€ remains active and can re-trigger, it can still cause spikes later — just less frequently.

  2. View changes that re-activate many conditions at once (e.g. switching to a heavy section) can cause mini-storms, though smaller than startup.

But you are not in a situation where the app is constantly burning CPU or degrading over time.


Why this is actually good news

This means:

  • You don’t have a fundamental architectural collapse

  • You don’t need to rewrite the app

  • You’re not flirting with browser limits

  • You’re dealing with a front-loaded convergence problem, which is one of the most fixable classes of performance issues in Bubble

FCP on the page is c.5 sec. Then the loader shows which is hidden automatically when page is loaded - which takes a couple more seconds. Juts did a full cycle in chrome in 9.7s with fcp in 4.7 but started from a loaded page.

Anyway I’m a bit out of my technical debt here but it is definitely interesting to gradually learn more about the performance impact of certain choices with respect to behavior in the browser.

My initial issue of having certain floating groups ā€˜stuck’ in a reusable element without being able to breaking them out of that nested reusable frame (and behaving relative to the page instead of the reusable group) should be resolved by converting these groups in into a global element and triggering the element parameters etc. instead of Display data in a group and reading the group..

Example I have filters on RGs (dropdowns) in a floating group. There is a wide variety of RGs all with different data types etc so more easy to create new RGs then trying to create multiple tangled complicated data type/display containers in one group (or is this bad practice?) associated with a huge variety of source conditions (this will get very messy I think). SO e.g. 20 RGs means 20 different filter sets, now in 20 floating groups, being directly able to read the Data Type or Option set on the filter as they are in the same reusable element, converting this information across elements at the same level is not possible so conversion of the elements to parameters will be required.

If I’m wrong, is there any best practices manual for this ?

I have 0 Clue what are you talking about, dropdowns are garbage if you populate them with data source it will load it even if it is not visible. Don’t use bubble built on page element, try not to use plugins from big corps, I don’t know I feel like I should be able to name them but people don’t so I won’t to be on the safe side,

Check your WU chart, you definetly leak, iff your rg leaks, donnt directly put data source ddo a search for ratherr put a conditional, then populate with data.

Use one font type, dont use every weight, Don’t ever ever ever use Bubble built google material icons plugin, it will destroy you. Stop trying to be smart, don’t try to do things Bubble native, if you try to work around groupfocus it is Bubble’s fault, Bubble native elements are bad, just use HTML, and js2bubble.

Don’t overfocus on lightening up the option sets.

Horizontally expand your data types.

I feel like you can solve all your problems with js2bubble and reusable properties and reducing leakage.

Bonus: Don’t use chatgpt, use gemini or claude

1 Like

I have been doing this for years and this is the most effective way to create a solid first load UX in Bubble. The principle behind it is to trigger UI visibility and data loads only on user actions. Data calls can create long pauses when they either return large datasets and/or when multiple data calls are happening at the same time.

There is another issue that gets overlooked by a lot of Bubblers, especially those building SPAs:

On Page Load events triggers for every ā€œGo to page > Current Pageā€ action.

Lock first page load actions to only occur once. Simplest is to just use a boolean state that triggers at the end of a first load sequence.

2 Likes

Thanks for your responses, this is helpful!

Just took a bit of a dive into workflows used most since the topic got me curious about my own app :slight_smile: and I noticed that most DRY issues I have, seem to relate to the pagination of my repeating groups.

I created a set of workflows for pagination and then basically copy/pasted them for every new repeating group that I have that requires pagination (as they would simply become unmanageable if I just had one RG with 100s of conditions being triggered by the required context).

The issue I experienced was I could not trigger changes in a repeating group if the pagination group would be in a reusable element, or, at least not without using the parameters on my page to trigger the pagination effects. Perhaps that was the way to set it up from the start but somehow I have the feeling that my continuous extension of parameters is not the way to do things… but maybe I’m wrong.

EDIT: Ok I’m wrong hehe not remotely close to fix the RG issue, so circling back to the 2000 + workflows (not counted them but every elements seems to easily have 100+ workflows front end) and with over 380 repeating groups and a lot using pagination. I think that accounts for a lot of the workflows required. I don’t see a real workaround as the Repeating group does not come with native navigation, nor with the possibility to select Dynamic sources so one RG could be reused ac couple of times. On the other hand these workflows don;t seem to impact performance, the only challenge is managing them within the Bubble, but that is ok-ish.

Pagnation should only be 3 wfs per rg

x 300 :slight_smile: