Pre-fetching tabs on SPA to radically improve navigation speed on

Also available in :


Victor Nihoul


Dec 29, 2022


🇬🇧 English

Have you been on a single-page app made on where changing tabs looked like this?

This behaviour is very common among Bubble apps, as Bubble will load data and render elements only when visible, so in most cases, too late.

Now, using tabs pre-fetching, here’s what we can have :

The user experience difference is simply… huge!
You don’t believe it? Try it yourself with the before and after previews.

Understanding pre-fetching

Pre-fetching, or in other words fetching/loading data in advance is a technique that consists in downloading data and rendering elements before the user needs them in a context where we’re almost sure they will need them at some point.

For example, if we have two pages “cart” and “checkout”, we’re almost sure the user will need the “checkout” page after being on the “cart” page, so we could pre-fetch the second page in advance.

In the tutorial below, we’ll see how we can apply this technique for single-page apps with multiple tabs.

Tab pre-fetching in

For this tutorial, we’ll take the following example:

  • A single-page app with two tabs.
  • The first tab will only contain basic elements such as non-dynamic texts.
  • The second page will contain repeating groups with nested API calls to simulate a complex tab.

Here’s the editor of the example app, and the current preview.

As I said earlier, Bubble will not load data and render elements before they’re visible to the user. Therefore, when navigating to the second tab with complex API calls, the user has to wait for a few seconds before everything shows up correctly.

We’ll use pre-fetching to force Bubble loading and rendering the tab before the user navigates to it.

1. Adding an initialization state

First, we’ll add an “is_initialize?” state to our page, with a default value set to yes/true.

Now, we want to force Bubble to load all tabs on page load using our custom state — when the “is_initialize?” state is set to yes.

For this to happen, I’ll add a conditional on all my tabs to set their visibility to visible when my state equals yes.

Note: as Bubble processes conditional in order (from bottom to top), make sure to add the conditional at the very bottom of your conditional list.

So now, all tabs will be visible on page load because the “is_initialize?” state has its default value set to yes.
This will force Bubble to load all the data from our tabs (including tab 2).

2. Removing the initialization state

Obviously, now that all tabs’ content is loaded, we still want to have our tabs work as they use to, so we need to set the “is_initialize?” state to no when all tabs are properly loaded.

This is done by adding a workflow event based on the conditional When page is loaded (entire) is yes which will trigger a custom event end_initialize to set back our custom state to no.

Scheduling the custom event when the page is fully loaded.
In the custom event, setting the state value to “no”.

Now, a few things you might be asking:

Why are we using the conditional When page is loaded (entire) is “yes” instead of the native event When page is loaded?

The When page is loaded event fires when the page becomes visible, and not necessarily when all the visible elements are fully loaded and rendered. Therefore, is it a better metric in our case to use the conditional When page is loaded (entire) is “yes”.
Note: depending on the use-cases, you might want to use the native event instead, we recommend to play around both solutions to see what fits best your use-case.

Why are we scheduling a custom workflow instead of setting the state value to “no” immediately?

This ensure other workflows that might run on page load to complete before we set the “is_initialize?” state value to “no”.

Let’s have a look at what we’ve done so far and the improvements we managed to implement:

This is great, it does exactly what we wanted, it first loads and renders all the tabs including their content before going back to its normal navigation, which has now become much faster — or I should stay instant!

The last issue we need to solve is the pre-fetching process — all tabs visible at first, which should not be visible to the user.

3. Adding a loader

To hide the magic, we’ll use a loader — a floating group on top, which will follow the same rule: it will be visible during the pre-fetching process when the “is_initialize?” state value is set to yes.

Adding a floating group on top acting as a loader.

But, remember the initial issue with Bubble?

[…] as Bubble will load data and render elements only when visible.

Therefore, applying a floating group on top will technically make all elements behind invisible, and therefore the tabs, even if theoretically visible, will not be loaded as Bubble will consider them hidden.

To break this last barrier, we’ll set the background colour property of the floating group to have 99% opacity, forcing Bubble to consider the background elements visible even if not visible to the naked eye.

Setting the floating group to have 99% opacity to force background elements’ visibility.


That’s it, we now have an entire pre-fetching system for our single-page app which considerably speeds up tab navigation.

Here’s what it looks like in its final version.

This indeed adds a few more loading milliseconds/seconds when the page first loads to the user, but in most cases, this is a better trade!