Practical optimization tips after 2 months of working with Anvil

Hi all!

I have been developing a web application with Anvil for the past couple of months. I must say, it’s awesome that I can build a full-fledged application using only (simple) Python for both the front end and back end. I’ve learned a lot by exploring the Forum and experimenting with different features. That’s why I want to synthesize all the learnings in this post so that other Anvil users can benefit. It might also help non-Anvil users make a better-informed decision on whether or not to invest in learning Anvil.

At a high level, the most impressive advantage of the platform is the abstraction it provides compared to libraries like Flask or Django. It has absolutely all the features you need to build an MVP (and probably beyond?). User authentication, event-driven workflows, drag-and-drop UI, background tasks, native integrations (Stripe, emails, Gmail…), secure databases, deployment infrastructure, and much more. Even though you could get all of these features by buying a Django or Flask template, you can’t beat the speed at which you can code new features with Anvil. The time from idea to production is probably 4 to 5 times faster than if I had to do it in Django/Flask. And for early-stage startups, the speed of experimentation is everything. Another note on Django/Flask: I still think it is useful to learn those frameworks because it will make development with Anvil even faster if you understand the basics of web application development.

Now for the downside: the speed and performance of the Anvil app. For most use cases, if you’re developing an MVP, speed is likely not the selling point, as long as you can keep the response time below 2 or 3 seconds, in my opinion. Which is possible with Anvil, but only if you optimize your code and think the Anvil way! Here are some tips I have found to help with performance. The performance gain is only informative and is intended to give you an idea of the order of magnitude.

1. Limit the number of server calls from your forms. (Response time from 10+ seconds to 3 seconds)

This one is unsurprisingly number 1. There are a lot of topics on the forum that deal with that technique. Reduce your server calls to 1 server call per form initialization and per user action. The tricky part is knowing when the server calls occur, apart from the regular “anvil.server.call” that you can search for.

  • When you call anvil.users.get_user
  • If your server function returns a search iterator, only some fields are pre-loaded (I believe only the text and date fields, but it needs verification). For other fields (e.g., simple object), they are loaded when you call them from the form, which incurs extra server calls. The solution is to write your server function to access the search iterator values and explicitly return what you need in your form.
  • A common but painful mistake is to load a search iterator via server call in your form and instantiate a repeating panel with the elements of the iterator. Then in the sub-form that represents one panel of your repeating panel, you will access the values of the search row, which incurs a server call. You can imagine that if you have 100 items in your repeating panels, that will add up to 100 additional, sneaky server calls. This is mitigated if you explicitly return all the values you need in your server function.

2. Use the accelerated tables and ALWAYS use the q.fetch_only feature (Response time from 10+ seconds to 3 seconds in some cases)

I have a table that contains a column with a lot of text, think the extracted text of a PDF with hundreds of pages. So each row in that table contains a large amount of text. But I don’t need that column anywhere in the UI, only in background tasks. So what I did was limit my forms to call only one server function and call it a day, but the web app was still extremely slow (think 15 seconds to load 200+ rows in a repeating panel). The issue was that even if I didn’t return the large text column to my form, it was still loaded somehow in the backend/server function. By using q.fetch_only, the performance went back to “normal” (3s). Also, not showing that column in the database UI speeds up your Anvil UI for obvious reasons.

So from now on, I always use q.fetch_only in my requests. It also makes the code more readable and robust to changes. Kind of similar to SELECT * vs. SELECT explicit columns in SQL.

3. Use background tasks as much as possible (from 3 seconds to 2 seconds)

If you are developing workflows that do not require real-time updates, consider modifying the code to utilize a background task instead. You can then choose to either display the status of the background task to the user if deemed necessary or simply carry out the update in the background, allowing the user to discover it later.

The performance improvement may not be significant since it’s typically not the bottleneck (the call to the server function is). However, it can be beneficial in certain scenarios. Particularly when you’re uncertain about whether to use a background task, opting for one is advisable.

4. Use caching (from 3 seconds to instant in the cases where caching is used)

I’m currently using two caching methods in my app. The first one is the basic caching mechanism in Python, and the second involves utilizing Anvil Extras’ routing feature. This feature allows you to specify which pages should be reloaded from the cache or from scratch. The key benefit, in my opinion, is providing users with an instant response, particularly when faced with frequent slow 3-second responses. This approach helps create a perception that the app isn’t consistently slow.

5. Use the Assets folder to load images from

I haven’t tested that tip yet because I don’t have many images, but I’ve read that you should upload all your images to the Asset folder first. Then, use the path to that Asset in your image component instead of loading from an external website.

6. I’m using Uplink to serve the API endpoints (from 4 seconds to 0.5 seconds)

If you don’t have the Business plan with dedicated servers, each call to an API endpoint you’ve coded with Anvil launches a new server and has to load all the modules of your entire app. The faster the endpoint responds is maybe 2 seconds, and more often than not can get to 5 or 6 seconds, which is way too slow for a basic endpoint (e.g. getting a specific row and returning it). I can’t share the performance details for the business plan, but I can share my workaround.

I created a separate Python module using the Uplink server connection. It has almost the same code as in Anvil but is modified to be self-contained. There is no anvil.server.call at all, but there are some background task runs. Essentially, I had to duplicate some code, both in Anvil and in the external Python module. So when I run this Python module from an external machine (e.g. on the cloud, or even on your computer), the API calls are instant (0.5 seconds)! The reason is that there is no need to launch a separate server for each API call. The server is already loaded with the endpoints ready to work.

This thread provides a lot of interesting details. I maintain duplicate code in both Anvil and my Python module so that the endpoints are present in both places. If my Python module goes down for any reason, then the Anvil code takes over to handle new API calls. Even if the response time goes back up to 3 or 4 seconds, it’s a good backup. This method seems to reproduce the behaviour you would get with the business plan, but at a much lower cost! (the business plan is $300+, while the pro plan is $54 + a server on Render is $12).

I’m now considering whether this workaround is something I can use more often when I need to scale other parts of my app. Should I duplicate other functions to external Python modules and connect the modules via Uplink? It’s certainly less clean and involves more maintenance work, but the improvement in performance is significant. If you have any ideas or feedback on this topic, I’d love to discuss!

The end

There are all my tips! Hopefully it will help some of you. Don’t hesitate to share yours too!

And if you want to test my app, you can here: Curated ML. It will only be useful for you if you work in data science / machine learning…

I’ll share all the threads that have helped me in the comments (I can’t post more than 2 links?).

15 Likes

Yeah, its a forum limitation for new users. @Quoc if you want to DM me the links to those threads that helped I will happily re-post them here.

When we post thread links in one thread they get referenced in that other thread as linked, it might help people find/come here to get this helpful synopsis.

Good tips! I just learned about caching in the anvil-extras routing module and wanted to see how it interacts with repeating panels.

I’m personally scared off of uplink in production scenarios due to the maintenance involved, but it looks like you have a robust setup when uplinks go down.

Your app is cool! Nice idea.

Questions

  • Did you create your chrome extension using Anvil? I’d love to know more about that.
  • Did you customize your welcome email in Anvil or did you use another tool for that?

A few notes:

  • If you care about mobile, there are a few things you can do to make sure the top app bar color (safe zone on iOS) and nav bar colors are consistent with the app background. One is editing the theme-color meta tag in the HTML header of your app. The default behaviour also changes the hue of the nav bar when you’re scrolled and doesn’t look great on mobile either. I just remove that behaviour on some of my apps.
  • You’ll want to update anvil-extras to the latest as there was a bug in the switch component that has now been fixed (switch bar is disappearing on select).
  • One little possible “bug” I messaged you on Crisp about
1 Like

Thanks Ian! I tried again and it seems to work now.

A couple of threads that have helped me:

Thanks Yahia for the feedback!

The anvil-extras routing caching module is great and seems to work well with data grid / repeating panels. For example, one of the page has a data grid with 200+ rows, and the cache can handle it with no issues. So that page is shown instantly.

I also don’t like the maintenance and reliability issues with the Uplink module, but I prefer that to slow API calls. I’ll let you know if I have to change the approach when I have more users. I’m guessing that if you really need the reliability, you can opt in for the Business plan which should provide the same (better?) results.

Questions:

  • I created the Chrome extension without Anvil. So I had to learn about the Chrome manifest and permissions, service workers, a bit of javascript and some more. Depending on the complexity of your extension, I’d say you could get started pretty quickly if you keep the extension simple. For more complex ones, you might need to spend more time learning about html/javascript + chrome extension specifics. Let me know if you want to discuss that more! I’ve struggled a bit too when I started learning about extensions.
  • The welcome email you received is not sent with Anvil. I use an external Email Marketing provider (EmailOctopus). I just wanted to have a no-code tool to personalize the emails and create sequences of emails based on user actions. So my setup is that I use Anvil emails for transactional emails only, and I send data to EmailOctopus via API so that I can personalize email outreach with pretty templates. I’m sure you could do the same thing entirely with Anvil if you create your own HTML templates + your own workflow rules + some background tasks here and there.

Thanks for the notes! I messaged you back on Crisp for the bug. The bug is actually related to the Uplink module, so you were right about the maintenance involved + more bugs :sweat_smile:
As for mobile, I’m not there yet, but I will definitely add your fix in my backlog for the future!

1 Like