-
-
Notifications
You must be signed in to change notification settings - Fork 508
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
83d5753
commit 7029343
Showing
3 changed files
with
197 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
# Defer Bound Functions to Improve the User Experience | ||
|
||
This guide addresses how to defer long running, bound and displayed functions with `defer_load`. You can use this to improve the user experience of your app. | ||
|
||
If you need to defer and orchestrate multiple, dependent tasks then check out the [Defer Long Running Tasks Guide](defer_load.md). | ||
|
||
--- | ||
|
||
## Motivation | ||
|
||
When a user opens your app, the app is *loaded* as follows | ||
|
||
- the app file is executed | ||
- the app template is sent to the user and rendered | ||
- a web socket connection is opened to enable fast, bi-directional communication as your interact with the app. | ||
|
||
Thus any long running code executed before the app is loaded will increase the the waiting time before your users see your apps template. **If the waiting time is more than 2-5 seconds your users might get confused and even leave the application behind**. | ||
|
||
Here is an example of an app that takes +5 seconds to load. | ||
|
||
```python | ||
import time | ||
import panel as pn | ||
|
||
pn.extension(template="bootstrap") | ||
|
||
|
||
def some_long_running_task(): | ||
time.sleep(5) | ||
return "# Wow. That took some time. Are you still here?" | ||
|
||
|
||
pn.panel(some_long_running_task).servable() | ||
``` | ||
|
||
![panel-longrunning-task-example](https://assets.holoviz.org/panel/gifs/long_running_task.gif) | ||
|
||
Now lets learn how to defer long running tasks to after the application has loaded. | ||
|
||
## Defer all Tasks | ||
|
||
Its easy defer the execution of all bound and displayed functions with | ||
`pn.extension(..., defer_load=True)`. | ||
|
||
```python | ||
import time | ||
import panel as pn | ||
|
||
pn.extension(defer_load=True, loading_indicator=True, template="bootstrap") | ||
|
||
def long_running_task(): | ||
time.sleep(3) | ||
return "# I'm deferred and shown after load" | ||
|
||
pn.Column("# I'm shown on load", long_running_task).servable() | ||
``` | ||
|
||
![panel-defer-all-example](https://assets.holoviz.org/panel/gifs/defer_all_tasks.gif) | ||
|
||
## Defer Specific Tasks | ||
|
||
Its also easy to defer the execution of specific, bound and displayed functions with `pn.panel(..., defer_load=True)`. | ||
|
||
```python | ||
import time | ||
import panel as pn | ||
|
||
pn.extension(loading_indicator=True, template="bootstrap") | ||
|
||
|
||
def short_running_task(): | ||
return "# I'm shown on load" | ||
|
||
|
||
def long_running_task(): | ||
time.sleep(3) | ||
return "# I'm deferred and shown after load" | ||
|
||
|
||
pn.Column( | ||
short_running_task, | ||
pn.panel(long_running_task, defer_load=True, min_height=50, min_width=200), | ||
).servable() | ||
``` | ||
|
||
![panel-defer-specific-example](https://assets.holoviz.org/panel/gifs/defer_specific_task.gif) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,41 +1,124 @@ | ||
# Defer Callbacks Until Load | ||
# Defer Long Running Tasks to Improve the User Experience | ||
|
||
This guide addresses how to set up callbacks to defer a task until the application is loaded. | ||
This guide addresses how to defer and orchestrate long running background tasks with `pn.state.on_load`. You can use this to improve the user experience of your app. | ||
|
||
--- | ||
|
||
Using the `onload` callback, we can trigger execution when a session is first initialized in a server context. An example of when this could be a helpful strategy is when we have to fetch something from some database, like the options that will go into a selection widget. Since this operation might take some time, we can quickly render something on the screen for the user to look at while the `onload` callback is continuing to fetch the options in the background. | ||
## Motivation | ||
|
||
Let us for example define a minimal example inside a function which we could pass to `pn.serve` (this emulates what happens when we call `panel serve` on the command line). In this example, we will create a widget without populating its options, then we will add an `onload` callback, which will set the options once the initial page is loaded. | ||
When a user opens your app, the app is *loaded* as follows | ||
|
||
```{pyodide} | ||
- the app file is executed | ||
- the app template is sent to the user and rendered | ||
- a web socket connection is opened to enable fast, bi-directional communication as your interact with the app. | ||
|
||
Thus any long running code executed before the app is loaded will increase the the waiting time before your users see your apps template. **If the waiting time is more than 2-5 seconds your users might get confused and even leave the application behind**. | ||
|
||
Here is an example of an app that takes +5 seconds to load. | ||
|
||
```python | ||
import time | ||
import panel as pn | ||
|
||
pn.extension() | ||
pn.extension(template="bootstrap") | ||
|
||
layout = pn.pane.Markdown() | ||
|
||
def some_long_running_task(): | ||
time.sleep(5) # Some long running task | ||
layout.object = "# Wow. That took some time. Are you still here?" | ||
|
||
some_long_running_task() | ||
|
||
layout.servable() | ||
``` | ||
|
||
![panel-longrunning-task-example](https://assets.holoviz.org/panel/gifs/long_load.gif) | ||
|
||
Now lets learn how to defer long running tasks to after the application has loaded. | ||
|
||
## Defer a Task | ||
|
||
```python | ||
import time | ||
import panel as pn | ||
|
||
def app(): | ||
widget = pn.widgets.Select() | ||
pn.extension(template="bootstrap") | ||
|
||
def on_load(): | ||
time.sleep(2) # Emulate some long running process | ||
widget.options = ['A', 'B', 'C'] | ||
layout = pn.pane.Markdown("# Loading...") | ||
|
||
pn.state.onload(on_load) | ||
def some_long_running_task(): | ||
time.sleep(5) # Some long running task | ||
layout.object = "# Done" | ||
|
||
return widget | ||
pn.state.onload(some_long_running_task) | ||
|
||
# pn.serve(app) # launches the app | ||
layout.servable() | ||
``` | ||
|
||
Alternatively, we may also use the `defer_load` argument to wait to evaluate a function until the page is loaded. In a situation where page loading takes some time, a placeholder and the global `config.loading_spinner` will be displayed. | ||
![panel-onload-example](https://assets.holoviz.org/panel/gifs/onload_callback.gif) | ||
|
||
Note that `pn.state.onload` accepts both *sync* and *async* functions. | ||
|
||
This example could also be implemented using a *bound and displayed function*. We recommend using that method together with `defer_load` when possible. See the [Defer Bound and Displayed Functions Guide](defer_load.md). | ||
|
||
## Defer and Orchestrate Dependent Tasks | ||
|
||
Sometimes you have multiple tasks that depend on each other and you need to *orchestrate* them. To handle those scenarios you use `pn.state.onload` to defer background tasks and `pn.bind` to trigger *bound and displayed* functions when the the background tasks have finished. | ||
|
||
Lets take an example where we | ||
|
||
- load a shared dataset. | ||
- display the dataset in a Table | ||
- transform the dataset and display it as a plot | ||
|
||
```python | ||
import time | ||
import panel as pn | ||
import pandas as pd | ||
import param | ||
import hvplot.pandas | ||
|
||
pn.extension(sizing_mode="stretch_width", template="bootstrap", theme="dark") | ||
|
||
class AppState(param.Parameterized): | ||
data = param.DataFrame() | ||
|
||
def update(self): | ||
time.sleep(2) | ||
state.data = pd.DataFrame({"x": [1, 2, 3, 4], "y": [1, 3, 2, 4]}) | ||
|
||
def loading_indicator(label): | ||
return pn.indicators.LoadingSpinner( | ||
value=True, name=label, size=25, align="center" | ||
) | ||
|
||
def short_running_task(): | ||
return "# I'm shown on load" | ||
|
||
def table(data): | ||
if data is None: | ||
return loading_indicator("Loading data") | ||
|
||
return pn.pane.DataFrame(data) | ||
|
||
def plot(data): | ||
if data is None: | ||
yield loading_indicator("Waiting for data") | ||
return | ||
|
||
```{pyodide} | ||
yield loading_indicator("Transforming data") | ||
time.sleep(2) # Some long running transformation | ||
yield data.hvplot() | ||
|
||
def render_on_load(): | ||
return pn.widgets.Select(options=['A', 'B', 'C']) | ||
state = AppState() | ||
pn.state.onload(state.update) | ||
|
||
pn.Row(pn.panel(render_on_load, defer_load=True)) | ||
pn.Column( | ||
short_running_task, | ||
pn.bind(table, data=state.param.data), | ||
pn.bind(plot, data=state.param.data), | ||
).servable() | ||
``` | ||
|
||
## Related Resources | ||
![panel-onload-dependent-tasks-example](https://assets.holoviz.org/panel/gifs/onload_dependent.gif) |