[Wiki] Performance optimization

This page addresses performance optimization.

The list in this page is far from being complete, there may be some incorrect information and it’s likely to become stale and obsolete over time. Feel free to fix content, language or format, anything you don’t like and add anything missing.

If there is something that you don’t understand, please create a new post and ask for clarification. Then, once the topic is clear, you can edit this page and improve it.

Some points are inspired by forum posts and include the link.


Get all you need with one server call

Round trips between the form and the Anvil server are usually the bottleneck.

Create one server function that return all the data you need on the client at once.

This way you have one round trip only. It might be slightly slower, but it will usually be much faster than splitting the work into several server side functions that require several round trips.

Bad example

# on the client
    self.title.text = anvil.server.call('get_title')
    self.cookies.items = anvil.server.call('get_cookies')
    self.candies.items = anvil.server.call('get_candies')

# on the server
@anvil.callable
def get_title():
    return 'Hello'

@anvil.callable
def get_cookies():
    return app_tables.cookies.search()

@anvil.callable
def get_candies():
    return app_tables.candies.search()

Good example

# on the client
    data = anvil.server.call('get_all_at_once')
    self.title.text = data['title']
    self.cookies.items = data['cookies']
    self.candies.items = data['candies']

# on the server
@anvil.callable
def get_all_at_once():
    return {
        'title': 'Hello',
        'cookies': app_tables.cookies.search(),
        'candies': app_tables.candies.search()
    }

See here for the original post.


Use dictionaries instead of SearchIterator or Row objects on the client

The client allows to manage data table rows, queries and views, both in read-only or read-and-write. This feature can be convenient in some cases, but often managing data table objects on the client can cause performance degradation for the following reasons:

  • A round trip is usually used to retrieve the query object. For example this:

    @anvil.callable
    def get_cookies():
        return app_tables.cookies.search()
    

    returns a SearchIterator object. The SearchIterator knows how to get data from the database, but has not done it yet

  • Later the client will iterate the SearchIterator and trigger one round trip and return Row objects. Anvil tries to limit the number of round trips by fetching around 100 rows at a time, even if the client needs only one, but iterating through many rows will trigger more than just one round trip

  • Later the client will get the values of the row fields. Some of them were included in the round trip that returned the Row object, but some will trigger another round trip. For example simple object columns, media objects or linked table columns are not automatically loaded, they always require a round trip

So rows = anvil.server.call('get_cookies') needs one round trip, then row = rows[0] needs another one, then cookie_color = row['color'] doesn’t need one because text fields are automatically loaded, but row['seller']['address'] will require another round trip to fetch the linked row in the sellers table.

The solution to avoid the proliferation of round trips when working with SearchIterator and Row objects on the client is not to work with SearchIterator and Row objects on the client.

Use dictionaries instead.

Bad example
The following code will trigger one round trip when explicitly calling the server function, and may (or may not) trigger a myriad of round trips when the repeating panel template forms access simple object or linked table rows:

# on the client
    self.repeating_panel.items = anvil.server.call('get_cookies')

# on the server
@anvil.callable
def get_cookies():
    return app_tables.cookies.search()

Almost good example
This example is almost but not really good because it converts to dictionary the whole row. This will include simple object columns, even if not required, and will not include linked table columns, even if required:

# on the server
@anvil.callable
def get_cookies():
    return [dict(row) for row in app_tables.cookies.search()]

Good example
Here the server function takes care of picking the required data, avoids returning unused columns and does include linked table values:

# on the server
@anvil.callable
def get_cookies():
    return [{
        'shape': row['shape'],
        'color': row['color'],
        'seller_address': row['seller']['address'],
    } for row in app_tables.cookies.search()]

Unfortunately returning a list of dictionaries to the client is not the same as returning a SearchIterator generator that “lazily” returns rows as needed.[1] The returned list must contain all the rows required by the client with something like ...for row in app_tables.cookies.search()[:50]]. This could be a problem on a paginated DataGrid: returning too few rows would limit the number of available pages, returning too many would make the round trip slower.

[1] It is possible to create a Portable Class that behaves like a generator - See the ORM library).

See here for the original post.


Delay loading the bulk of the data

When the server call that loads a long list of dictionaries from the server is too slow, make two server calls, one for the items that are required immediately and one for the remaining items.

The first call happens as usual in form_load or in __init__.
The second call happens in a timer_tick event of a timer with interval set to 0.1.

Example
Here the form_load event gets 20 items from the server, assigns them to the repeating panel’s items member, then Anvil populates the template forms and renders the result. At this point the user is happy because they see the result very quickly. While the user looks at the first page, the tick event loads the next 180 items. This will be as slow as it would be loading everything at once, but it happens while the user is happy, not while the user is frustrated by a slow form:

def form_load(self, **event_args):
    self.repeating_panel.items = anvil.server.call('get_items', 0, 20)
    self.timer.interval = 0.1

def timer_tick(self, **event_args):
    self.timer.interval = 0
    self.repeating_panel.items = self.repeating_panel.items + anvil.server.call('get_items', 20, 200)

(This way of adding 180 items to the 20 items may trigger the creation of the 20 forms already rendered. Perhaps this works just fine, perhaps this is OK because it’s invisible, perhaps this should be updated in a different way. This is an old post, I should do some tests to verify.)

See here for the original post.


Replace the Repeating Panel in a Data Grid with a Linear Panel

By default a Data Grid contains a Repeating Panel. Assigning a large list of items to the Repeating Panel can be slow. Adding items when a new page is needed would trigger the deletion and re-generation of the forms already generated.

By adding items to a Linear Panel when needed, the client side code can load more items and add more forms while avoiding re-rendering the existing ones. Linear Panels can be used inside a Data Grid and they can play nicely with the columns defined in the Data Grid.

(This post is old. Today the Data Grid is faster but using Linear Panels can be still helpful.)

See here for the original post.


Move slow server imports inside the callable function

Unless you enable Persistent Server Modules (Business Plan and above only), each server function call spins up a new Python interpreter. That is, each time you call any server function via anvil.server.call, all your server modules will be loaded anew. Demo clone: here

As a result, moving any slow imports (e.g., plotting libraries) inside the specific callable function that needs them can significantly speed up server calls to other functions that do not use those imports.

See here for one original post on this.

13 Likes