Here’s a super simple idea to help improve performance:
Instead of always re-running from the top, why not allow users to define checkpoints?
The idea would be that whenever an app (or a page, for MPAs) reaches a checkpoint, it only re-runs from there (instead of re-running from the top). It could be as simple as calling something like st.checkpoint()
. This paradigm is a lot simpler to use than conditional re-rendering (which could still be useful for more advanced use cases). It’s the same paradigm as re-running from the top, except you’re only re-running a smaller portion of the app (from a “lower top”).
The best example would probably be something like a wizard: after each step, st.checkpoint()
is called, and only the next step is rendered.
Or it could be used to define a header for the app, and effectively disabling re-rendering of the header.
Alternatively, it could also be used as a form caching, by calling st.checkpoint()
after fetching some data. The rest of the app could then operate on this data without having to re-fetch it constantly.
A series of checkpoints would effectively split the app into portions (almost like “sub-apps”, or cells in a notebook). For example, two checkpoints would split the app in three:
- Before checkpoint 1
- Between checkpoint 1 & checkpoint 2
- After checkpoint 2
Also, checkpoints could be given a name (possibly mandatory), which would allow them to be re-defined in conditional logic; for example: st.checkpoint('header')
. I don’t quite have a specific use case for this, but why not. If anything, it could be useful to name checkpoints simply as a form of documentation.
In the app, each checkpoint could be rendered as a div
(or some other “invisible” container), containing everything “above” it (up to the previous checkpoint, if any).
When interacting with a component, the trick would be to detect in which portion of the app that component is (i.e. which div
/container), and re-run the app not from the latest checkpoint, but from that component’s checkpoint.
For example, in this setup, interacting with component 2 does not cause a re-fetching of the data, or a re-rendering of component 1, but does cause a re-rendering of component 2 & 3:
- Fetch some data
- Component 1
st.checkpoint('header')
- Component 2
st.checkpoint('filters')
- Component 3
If necessary, a “refresh” button could be added to the header portion, which would force a re-rendering of the entire app (or added to any portion, forcing a re-rendering of that portion onward).
Alternatively, the api could be defined as st.portion_start('name')
& st.portion_end('name')
, which may be more intuitive for some users (rather than naming a portion at the end of it). But st.checkpoint('name')
feels more “streamlitic” to me.