[Tutorial] Pre-fetching tabs on SPA to radically improve navigation speed

Hello Bubble forum :sweat_smile:
Here is another free Christmas gift for the community :christmas_tree:


Have you been on a single-page app made on Bubble.io 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 Bubble.io

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.

Conclusion

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!


If you want to dig further, here are some sweet options:

Extras:


Victor from Flusk

Black_512x512 Flusk - a hub of tools and services for Bubble makers and businesses

14 Likes

@drixxon Do you have more details about what caused your app to beak when applying the initialize state on every group?
I’ll be really curious to explore that :yum:

Dude this is super good content!
Thanks for sharing this tip! :star_struck:

I think lot of people will overlook on the value this improvement can make!

Glad it helped! :yum:
Feel free to share videos of the improvements if you get a chance to!

1 Like

I don’t mean to be negative here (and I will actually remove this post if folks don’t think it fits in with what I am sure will otherwise be a love-fest for this tip, and probably rightly so), but I think this statement at the end of the post is being downplayed a bit.

In your simple demo, the tip seems to add at least three seconds to the initial load time. So, in a real app, it could easily add half-a-dozen to even tens of seconds to the initial load time. (It was quite a while ago, but even Emmanuel himself advised against this tip for that reason.)

While some (most?) folks might agree that front-loading all of the load time is a better trade than spreading the load time out across the tabs when they are loaded for the first time (and only the first time, so an app that uses the tip or an app that doesn’t use it will perform the same after the bigger initial load or smaller loads across the tabs are done), I have worked with a number of UX folks throughout my career who are definitely opposed to the longer initial load time.

To be clear, it’s still a great tip, and one of the best parts is it can be tried in an app with little effort to see if the trade off (initial load vs. smaller loads across the tabs) is worth it. I have seen cases where it is not, though, and again, it felt (maybe only to me) like that pretty important aspect of the tip might get missed.

6 Likes

Thanks, that’s a very relevant point!

Not really, because you don’t need to wait until the elements and data are fully loaded to end the pre-fetching process.
Indeed, the trick here is simply to force the browser to start requesting the necessary data, but as soon as started, this process will run in the background as elements’ visibility won’t affect existing fetching requests.

You can test this by adding a very heavy file to an image element on Bubble, which you’ll hide as soon as displayed, and observe the results in the network tab of the Chrome console

So there should be no difference in load time regarding how many tabs, elements or data you’re collecting.

:smiley:

I definitely understand this point in theory, but I am skeptical that it would work out like that in practice. That’s probably just the skeptic/cynic (he prefers realist) in me doing what he does best, but damn, that guy can really be a pain in the ass sometimes.

It might be interesting (as you said) to explore what Perfect ran into because what he described is what I was thinking could happen. Again, though, still a great tip all around.

You could also get best of both words by going further, this is something we usually implement for clients:

  1. Load the page with normal behaviour, this way you keep the initial load time unchanged
  2. As soon as necessary elements/initial tab is loaded, start prefetching all other tabs (using invisible copies of the other tabs elements data)

Let’s debunk this with facts then :yum:
On an example I made here, I worked with 3 tabs, with a large picture on each.

Using pre-fetching as implemented according to my tutorial, here’s what it looks like:

It took 11 seconds to show the first tab, after which the other tabs kept being fetched for 15 additional seconds (3 seconds for tab 2 and 15 seconds for tab 3)

Meaning, supposing the user switched to tab 2 after using tab 1 for at least 3 seconds, he won’t experience any load time when switching tabs.

Now, here’s what it looks like if we’re not using pre-fetching:

The first tab took 8 seconds to load (so indeed 3 seconds less than with my method)

→ Note that I made an example with 83 MB of data to make the difference more explicit, but this should rarely happen on a Bubble app, and the long load time using pre-fetching is explained by concurrent data fetching (which is not affected by quantity - versus size here) so the difference will rarely (or never?) reach these 3 seconds.

Now, when switching tabs:

We observe an additional 10 seconds for tab 2 to load and 20 seconds for tab 3 to load.


So in the end:

Not using pre-fetching
  • TAB 1 initial load = 8 seconds
  • TAB 1 to TAB 2 = 10 seconds
  • TAB 2 to TAB 3 = 20 seconds
  • Total load time for the user: 38 seconds
Using pre-fetching
  • TAB 1 initial load = 11 seconds
  • TAB 1 to TAB 2 = 0 seconds
  • TAB 2 to TAB 3 = 0 seconds
  • Total load time for the user :fast_forward: 11 seconds
Using pre-fetching

Admitting the user immediately clicks on another tab when one is loaded

  • TAB 1 initial load = 11 seconds
  • TAB 1 to TAB 2 = 2 seconds
  • TAB 2 to TAB 3 = 13 seconds
  • Total load time for the user: 26 seconds
3 Likes

Yup, that concept has been around for a while, and I have used it in a number of apps, too… it’s a good one for sure.

Thanks for taking the time to make the example… very interesting and much appreciated.

Great tip! I apply similar loading tricks coupled with some parallel processing to speed things up in complex parts of my ERP.

@mikeloc is right though. There is a point, in Bubble land especially, where pre-fetching would not be a good idea. Especially when it involves large search results or an element that depends on (and hence triggers) large search results.

For example a repeating group that loads a hundred items (which usually won’t be any issue at all) but those hundreds items contain fields with large lists each will be more detrimental to the user experience than without pre-loading.

The point is that when it comes to Bubble there’s no one size fits all method to load optimization. It’s dependant on a lot of things, as can be found in plenty of posts over the years, and a beginner that decides to pre-fetch everything is gonna have one hell of a headache moving forwards.

2 Likes

Definitely!
It depends on use-cases and what you’re trying to achieve :tada:

Love the discussion on this thread. :slight_smile:

I think to add on to @vnihoul77’s tip, and to address @mikeloc and @ihsanzainal84’s points;

Even at the point at which pre-loading becomes ill-advised, part of @vnihoul77’s tips can still be applied, where he applies a floating group with a loading spinner to cover the page.

That is better UX from a user perspective than a page that has elements and images appearing one by one, as is the case when Bubble applies lazy loading.

Thanks for your comment :+1:

Regarding Bubble’s new version 21 Performance update :

As you were a few to ask me in PM, this update is not affecting the behaviour of pre-fetching, as elements are still considered visible by Bubble from a user perspective and therefore are still pre-fetched.
Just make sure the Fetcher RG is somehow visible (eg. 1x1px on the page)