Form takes > 25s to load without any server calls (long javascript evaluation time)

Hello,

First of all a big thanks to the anvil team for making such an awesome product. I am facing some loading time challenges and was wondering if anyone could point me towards potential solutions.

One of my forms has an awfully long loading time (>25 seconds). The form does not make any calls to the server so the long loading time is purely on the client side. I’ve published a copy of the form here - https://mal-wrapped.anvil.app/. I do not want to make the app public at this point but I can share the code with a staff member if they would be willing to have a look.

The form has a number of components and ~600 lines of python code. After the initial load time, the form is quite responsive and quickly reacts when events are triggered (e.g., changing the year or selecting a genre in the genre breakdown).

I ran some tests to try to diagnose the poor loading time and have some idea of what the issue is.

I ran a test on pagespeed.web.dev and it seems that the main culprit is 24 seconds of script evaluation time.

More specifically, the /_/static/runtime/node_modules/jquery/dist/jquery.min.js?sha=********** script was responsible for 95% of this script evaluation time (see image below). I assume that this is the file compiled by skulpt from the form’s python code (perhaps @stucork can confirm this)?

Perhaps it’s also worth mentioning that this file is many times smaller than say plotly.min.js (87.4 KiB vs 3.4 MiB) but the script evaluation time is ~20 times slower (22.7s vs 1.1s).

When I ran a test with a different tool (webpagetest.org) it provided me with a processing breakdown where it shows that the “TimerFire” event (or event category?) is responsible for ~93% of the processing time. I’m completely new to web development so TimerFire does not speak anything to me and googling it didn’t help.

I’m curious if anything could be done to improve the page’s loading speed. Or is this simply an anvil/skulpt limitation? Any help would be much appreciated.

Thanks in advance!
Zaro

If the problem is restricted to a form, you can start removing parts of the form (code or components) and see when it becomes fast.

2 Likes

Google Pagespeed insights may not be particularly useful in debugging your current problem. Maybe sharing some of your code will help us figure out what’s wrong (Only the code in __init__ is useful for the current problem.

Please note that not only server calls but also any app_tables query, external API calls or js libraries being imported will affect the loading time of the form

2 Likes

Very rough guess: Could this be related to one or more Timer components on your Form?

1 Like

@stefano.menci thanks!

From what I can tell there is nothing in particular that has a disproportionate impact on the loading time. Removing any given component along with its associated code only has a marginal improvement on the loading time. This leads me to believe that it is simply the number of components and/or size of the code. However, strangely enough, reverting to an early commit with only 2 RepeatingPanels, a dozen or so labels and ~40 lines of code it still takes 10-11 seconds for the form to load in chrome. But I will continue to dig deeper.

I was wondering if there are any tricks/optimizations that can be implemented which are independent of the particular code.

E.g., is it possible to cache the troublesome jquery.min.js file in the browser’s cache so that the JS does not have to be evaluated each time, and the user would experience the long loading time only the first time they visit the form? (I’m very new to web development so forgive me if I’m saying something stupid)

Or is it possible to “pre-load” the javascript of the form before the user actually visits the form? The form in question will not be the startup form in the completed app - the user would have to go through one or more other forms beforehand. Would it be possible to pre-load the required javascript when the user is still on one of these previous forms in the flow?

Perhaps it’s also worth mentioning that the form loads significantly faster in firefox than chromium-based browsers (8-10 secs vs 25-30 secs).

@divyeshlakhotia Thank you!

What I’m doing is loading some sample data (dict) from a client-side module. Hence, I am not using any anvil tables, nor making external API calls nor importing external JS libraries. But if I were, wouldn’t things like API calls show up in a different category on pagespeed insights rather than “script evaluation”?

There’s not much happening in the __init__, here is the code:

  def __init__(self, **properties):
    
    # Set Form properties and Data Bindings.
    self.init_components(**properties)
    # Set the Plotly plots template to match the theme of the app
    Plot.templates.default = config.plot_template

    # Populate selector and select initial value
    self.year_selector_dropdown.items = sorted(yearly_stats_dict.keys(), reverse = True)
    selected_year = max(yearly_stats_dict.keys())
    self.year_selector_dropdown.selected_value = selected_year

    # Create a dictionary with the easonal containers
    self.seasonal_highlights_containers = {
        'winter_highlights_container': self.winter_highlights_container,
        'spring_highlights_container': self.spring_highlights_container,
        'summer_highlights_container': self.summer_highlights_container,
        'fall_highlights_container': self.fall_highlights_container,
    }
    
    self.change_year(selected_year)

What I’m doing is simply populating the dropdown menu, creating a dict with some components so I can reference them with strings later on, and call the change_year function with the most recent year.

change_year is a long function that populates all of the components with the data for the selected year. The thing is that the same function is called whenever I change the year from the dropdown menu and it updates the form more or less instantly.

I’m also doing some things before the class declaration. I’m not sure if this is good practice or not (see image below)

Good guess but I am not using any timer components haha

Blindly optimizing without knowing what is the bottleneck that needs optimization rarely helps. You can put a Ferrari engine in a Ford Fiesta with a flat tire, and you are unlikely to reach 300 km/hr.

Jquery and all the other code is loaded at startup, not when a form opens, and it’s mostly very well cached. I’m pretty sure that is not slow.

The code is executed when events are triggered, there seem to be a function that spends to much time doing who knows what.

Try to sprinkle print(time.time()) at the beginning and at the end of each event handler. There are better ways, like a decorator from Anvil Extras, but even that simple print should tell you what comes before and after the long delay.

4 Likes

Otherwise, you can also try moving all the init code to form_show. This might help you track down the issue better since you can see the changes to your app as code gets executed.

2 Likes

Thanks everyone for the help. I identified that the problem was storing the user data as a global variable and importing it in the form code. The user data is relatively large in my case - a nested dict with ~25k lines and a file size of >700KB (it was the only variable in my Globals.py). Importing it was taking ~25 seconds.

Instead of storing the data as a global variable, I added it as a parameter to the form constructor and simply pass it as an argument when opening the form. It is much faster than importing it from Globals.


P.S. - for anybody curious, I also had an alternative setup where I was storing it in the session state and referencing the session state when passing it to the client. This was also quite slow. Now I directly pass it to the client without storing it in anvil.server.session and it is much faster.

I.e., when dealing with a large object, this

@anvil.server.callable
def return_user_data():
    user_data = create_user_data(...)
    return user_data

is much faster than this

def get_user_data(...):
    anvil.server.session["user_data"] = create_user_data(...)

@anvil.server.callable
def return_user_data():
    return anvil.server.session["user_data"]
2 Likes