The common knowledge is that :filtered will filter results client side. Is that actually true (spoiler, it’s not).

How does Bubble determine what data to fetch, and how much of it? If you’re looking to reduce your WU cost and increase data load times, understanding how this works is a prerequisite to be able to improve the performance.

How Bubble schedules searches

Once the page loads, the Bubble engine works out which expressions need database data and adds them to a queue of requests e.g 'fetch me all Tasks where XYZ, and Current User’s Workspace'.

After 10 milliseconds, or when the queue has 30 pending searches (whichever comes first), the queued searches are batched and run as one request. You can see this in your network tab as msearch - multiple searches, one round trip.

First off, two important things to understand here. References to the same data deduplicate - three elements all reading Current User's Workspace's name resolve to one fetch. And fetches are record-level, not field-level, so once a Thing is on the page, reading a fourth or fifth field from it costs nothing extra.

How results are paginated

What if we need 500 items, because the repeating group shows everything without lazy loading?

Bubble takes the first batch of up to 20 items and calculates the average size of each one. It targets a response size of 1MB. So if 20 items came back at 200KB, the next batch fetches 100 items. There is a hard cap of 400 items per batch - even if your Things are tiny, Bubble will not fetch them all at once.

The learning point: So, small Things (fewer fields, less data per field) reach the front end faster, because more fit in each batch and batches run in sequence. A list of tasks Task with a title, status, and due date will generally load faster than a list of Tasks with long notes, images, and histories, because the latter requires more round trips to load the same number of records.

Fetching things by ID

Not all database interactions are searches. Any expression like Current cell's Invoice's Company is fetching a Thing by its ID - the Invoice stores the Company's ID, and Bubble retrieves the record. You never write this lookup explicitly (though, I wish Get thing by ID existed!), it just happens whenever an expression uses a field relationship.

These are batched separately from searches: up to 200 pending ID lookups go out in a single request.

The :filtered decision

Here is the part people get wrong. Some developers use :filtered hoping it is free because it is 'filtering data that's already on the client'. Sometimes it is. But Bubble decides at runtime, and it goes roughly like this.

First, can every constraint in the filter run client-side? Basic comparisons like equals, not equal, contains, greater than, less than, is empty, can run client-side. But some constraints on a search must always go to the server:

  • Geographic filters (within X miles of Y)

  • text contains, contains keywords, and their negations

  • Any field

  • Range contains / range contains point

  • Email field equals and email field contains

If everything can be run client-side, then Bubble then decides whether finishing the load is cheaper than asking the server:

  • Under 100 items in total and already on the page? Filter locally.

  • Otherwise, if finishing the load needs fewer than 200 more items, or you have already loaded more than half and need fewer than 1,200 more, Bubble fetches the rest and filters client-side.

  • If completing the load would fetch more than 1.5x the data you actually need, Bubble skips the wasteful fetch and filters on the server instead.

So, Bubble will run :filtered on the client whenever it is possible and efficient. One caveat is that if you use :filtered on a list that is already loaded (a custom state, a plugin state), even the 'server-only' constraints can generally run client-side. Additionally, a few list field types always filter client-side regardless: lists of geographic addresses, date ranges, and number ranges.

What to do with all this

A few takeaways for you:

  • Keep your Things small. This is the same argument as satellite considerations elsewhere - the fewer bytes per record, the fewer round trips per page. Bubble rewards long tables with many small records, not wide tables with smaller number of heavy records.

  • Do not assume :filtered is free. Whether it hits the database depends on your constraints and how much data is already loaded.

  • Hidden elements can still cost you. If a hidden tab contains expressions that are required to evaluate on page load, those fetches will queued anyway. Put the data source behind a visibility condition so it only runs when the group is actually shown to guarantee it won’t be fetched.

Hope this is useful!

George

Founder, Not Quite Unicorns & Buildprint