Description
Hello,
First, thank you for your amazing work on Dash.
Describe your context
dash 2.17.1
dash_ag_grid 31.2.0
dash-bootstrap-components 1.4.1
dash-core-components 2.0.0
dash-html-components 2.0.0
dash-table 5.0.0
jupyter-dash 0.4.2
Browser: Firefox / Chrome (not related) on MacOS 14 (not related either).
Describe the bug
I recently came across a performance limitation of Dash that really made my app slow, and at some point, unusable.
The idea is that I needed to have N checkboxes that my user can check or uncheck. N can sometimes be 20, but it could actually go up to 500 or 1000. That’s where I hit some performance issue with Dash.
Expected behavior
I expect to have N > 1.000 checkboxes rendered in my webpage with no struggle. A similar Vanilla JavaScript code can handle more than 100.000 checkboxes before the page starts to get slow. I imagine that using React comes with some performance downgrade, but it seems to be optimizable.
Screenshots
Here is a visual example for the problem (for 500) ; you can see a lag between my clicks and checkboxes actually being checked:
Reproducible code
We create 500 checkboxes and get their value with one callback.
import uuid
import random
import dash
from dash import dcc, html
from dash.dependencies import Input, Output, ALL
app = dash.Dash(__name__)
# We create 500 checkboxes
checkboxes = [
dcc.Checklist(
options=[{"label": f"Checkbox {i+1}", "value": "checked"}],
id={"type": "checkbox", "group": random.choice(["a", "b", "c"]), "index": str(uuid.uuid4())},
inline=True
) for i in range(500)
]
app.layout = html.Div([
html.Div(id="output", style={"position": "fixed", "top": 0, "left": "200px"}),
html.Div(checkboxes)
])
# Client-side callback to illustrate that network is not the bottleneck here
app.clientside_callback(
"""
function(checkbox_values, checkbox_ids) {
const groupCounts = { a: 0, b: 0, c: 0 };
checkbox_values.forEach((value, i) => {
if (value && value.includes('checked')) {
const group = checkbox_ids[i].group;
groupCounts[group]++;
}
});
const totalChecked = groupCounts.a + groupCounts.b + groupCounts.c;
return `${totalChecked} checkboxes checked (Group A: ${groupCounts.a}, Group B: ${groupCounts.b}, Group C: ${groupCounts.c})`;
}
""",
Output("output", "children"),
Input({"type": "checkbox", "group": ALL, "index": ALL}, "value"),
Input({"type": "checkbox", "group": ALL, "index": ALL}, "id"),
)
# Run the app
if __name__ == "__main__":
app.run_server(debug=True, port=8060)
The issue is not with the callback itself, but occurs prior to the callback. It just takes some time to gather all the checkboxes, and then fires the callback (which runs fast).
Here are some things I noticed:
- performance drops after 100s of elements.
- it’s still slow even if I get rid of the groups and replace string indexes with integers
- CPU is between 90-100% and at some point the browser may ask to stop the script
- the problem is not related to the browser. I tested on Chrome and Firefox, recent versions.
- a similar vanilla JavaScript page (with pattern-matching-like IDs being parsed to JSON) can handle more than 100,000 checkboxes without any performance drop…
Here is the performance report (Firefox). I clicked 4 times, the CPU was around 90% all the time, making the page unresponsive:
One of the bottlenecks seems to be getReadyCallbacks
from dash_renderer.
I hope this can be solved!