Hi @AnnMarieW @Datenschubse,
Many thanks for your insights and super helpful feedback
. I worked on implementing some of your suggestions over the last 10 days. Here is a summary and some questions as well.
- Profiling & performance : @AnnMarieW, thanks for mentioning that tutorial based on Werkzeug. I used it to track performance issues and managed to implement some basic grouping/caching mechanisms to reduce API calls + started implementing dataframe caching with Redis as well. I’ll come back below on the remaining challenges & bottlenecks I identified. Performance feels better overall now.
- @AnnMarieW & @Datenschubse : Regarding theming, @AnnMarieW is right, mixing DBC & DMC forced me to implement some CSS changes that are problematic for performance. Right now, I disabled the theme change when you’re on the dashboard page, this needs to be done on any prior pages first. Still, the dash Patch you mentioned @Datenschubse was exactly what I was looking for to update figures theme, this is working fine and will be useful once I harmonised the grid system to DMC.
- Console issues & timeout: @Datenschubse, thanks for mentioning this, should be solve now except a warning for dash-ace I could not figure out. Regarding clientside callbacks timeout, I can imagine you identified this using the browser console, how would you optimise those timeouts in order not to block the UI ?
Profiling helped a lot in general but my understanding is that I should in the near future plan a design revision of my dash implementation. If I got it correctly, the way Dash works is that, the more complex are the callbacks and the number of the outputs, the larger is becoming the dash component tree, leading to performance issues.
I think this is visible in profiling analysis below for instance:
SUMMARY:
Total requests analyzed: 425
Slow requests (>50ms): 6
🐌 SLOWEST REQUESTS:
1. POST _dash-update-component
Duration: 731ms
File: POST._dash-update-component.731ms.1756200646.prof
2. POST _dash-update-component
Duration: 572ms
File: POST._dash-update-component.572ms.1756200768.prof
3. POST _dash-update-component
Duration: 542ms
File: POST._dash-update-component.542ms.1756200765.prof
4. POST _dash-update-component
Duration: 470ms
File: POST._dash-update-component.470ms.1756199927.prof
5. POST _dash-update-component
Duration: 258ms
File: POST._dash-update-component.258ms.1756200641.prof
📊 ENDPOINT PERFORMANCE:
_dash-update-component:
Avg: 48.1ms | Max: 731ms | Count: 76
_dash-component-suites.dash_ag_grid.async-community.js:
Avg: 7.3ms | Max: 12ms | Count: 3
static.screenshots.68ac6a55993caec967db80c5.png:
Avg: 7.0ms | Max: 12ms | Count: 2
_dash-component-suites.dash.dcc.dash_core_components.v3_2_0m1754906532.js:
Avg: 6.0ms | Max: 6ms | Count: 1
static.screenshots.6824cb3b89d2b72169309737.png:
Avg: 5.5ms | Max: 8ms | Count: 2
🔍 DETAILED FUNCTION ANALYSIS:
Request #1: _dash-update-component (731ms)
Total functions analyzed: 2471
📊 TOP FUNCTIONS BY TIME:
1. threading.py:1018(_bootstrap)
Time: 0.7309s (99.95%) | Calls: 1 | Per-call: 730.88ms
2. profiler.py:114(runapp)
Time: 0.7308s (99.94%) | Calls: 1 | Per-call: 730.81ms
3. app.py:2160(wsgi_app)
Time: 0.7308s (99.94%) | Calls: 1 | Per-call: 730.78ms
4. app.py:1471(full_dispatch_request)
Time: 0.7307s (99.92%) | Calls: 1 | Per-call: 730.69ms
5. app.py:1446(dispatch_request)
Time: 0.7306s (99.91%) | Calls: 1 | Per-call: 730.60ms
6. sync.py:164(__call__)
Time: 0.7306s (99.91%) | Calls: 1 | Per-call: 730.57ms
7. threading.py:323(wait)
Time: 0.7304s (99.89%) | Calls: 3 | Per-call: 243.48ms
8. current_thread_executor.py:63(run_until_future)
Time: 0.7304s (99.89%) | Calls: 1 | Per-call: 730.41ms
9. thread.py:54(run)
Time: 0.7303s (99.88%) | Calls: 1 | Per-call: 730.34ms
10. base_events.py:655(run_until_complete)
Time: 0.7300s (99.83%) | Calls: 3 | Per-call: 243.34ms
11. base_events.py:631(run_forever)
Time: 0.7300s (99.83%) | Calls: 3 | Per-call: 243.33ms
12. base_events.py:1922(_run_once)
Time: 0.7300s (99.82%) | Calls: 6 | Per-call: 121.66ms
13. runners.py:86(run)
Time: 0.7299s (99.82%) | Calls: 1 | Per-call: 729.94ms
14. events.py:86(_run)
Time: 0.7299s (99.81%) | Calls: 6 | Per-call: 121.65ms
15. ~:0(<method 'run' of '_contextvars.Context' objects>)
Time: 0.7299s (99.81%) | Calls: 6 | Per-call: 121.65ms
🧬 BYTEARRAY OPERATIONS:
1. ~:0(<method 'endswith' of 'bytearray' objects>)
Time: 0.0001s (0.01%) | Calls: 105
2. ~:0(<method 'split' of 'bytearray' objects>)
Time: 0.0000s (0.01%) | Calls: 15
3. ~:0(<method 'extend' of 'bytearray' objects>)
Time: 0.0000s (0.00%) | Calls: 1
📦 SERIALIZATION OPERATIONS:
1. basedatatypes.py:5652(to_plotly_json)
Time: 0.0210s (2.88%) | Calls: 16
2. _utils.py:24(to_json)
Time: 0.0181s (2.47%) | Calls: 1
3. _json.py:79(to_json_plotly)
Time: 0.0180s (2.47%) | Calls: 1
4. __init__.py:183(dumps)
Time: 0.0180s (2.46%) | Calls: 27
5. utils.py:154(encode)
Time: 0.0179s (2.44%) | Calls: 1
6. encoder.py:183(encode)
Time: 0.0178s (2.43%) | Calls: 27
7. encoder.py:205(iterencode)
Time: 0.0177s (2.43%) | Calls: 3
8. utils.py:240(encode_as_plotly)
Time: 0.0159s (2.17%) | Calls: 315
9. basedatatypes.py:3326(to_plotly_json)
Time: 0.0096s (1.31%) | Calls: 2
10. base_component.py:239(to_plotly_json)
Time: 0.0063s (0.87%) | Calls: 320
⚡ DASH/PLOTLY OPERATIONS:
1. dash.py:1467(async_dispatch)
Time: 0.7181s (98.20%) | Calls: 1
2. _callback.py:640(add_context)
Time: 0.6935s (94.84%) | Calls: 1
3. _callback.py:58(_invoke_callback)
Time: 0.5477s (74.90%) | Calls: 1
4. callbacks.py:18(display_page)
Time: 0.4763s (65.14%) | Calls: 1
5. app_layout.py:452(create_dashboard_layout)
Time: 0.2486s (33.99%) | Calls: 1
6. restore_dashboard.py:265(load_depictio_data_sync)
Time: 0.1915s (26.19%) | Calls: 1
7. restore_dashboard.py:50(render_dashboard)
Time: 0.1854s (25.35%) | Calls: 1
8. basedatatypes.py:5652(to_plotly_json)
Time: 0.0210s (2.88%) | Calls: 16
9. basedatatypes.py:2877(plotly_update)
Time: 0.0210s (2.87%) | Calls: 24
10. _json.py:79(to_json_plotly)
Time: 0.0180s (2.47%) | Calls: 1
🔢 NUMPY/NDARRAY OPERATIONS:
1. basevalidators.py:53(copy_to_readonly_numpy_array)
Time: 0.0039s (0.54%) | Calls: 60
2. series.py:1376(to_numpy)
Time: 0.0010s (0.13%) | Calls: 25
3. basevalidators.py:168(is_numpy_convertable)
Time: 0.0010s (0.13%) | Calls: 1,541
4. series.py:285(to_numpy)
Time: 0.0010s (0.13%) | Calls: 25
5. ~:0(<method '__deepcopy__' of 'numpy.ndarray' objects>)
Time: 0.0006s (0.08%) | Calls: 40
6. ~:0(<method 'reduce' of 'numpy.ufunc' objects>)
Time: 0.0004s (0.05%) | Calls: 48
7. series.py:4252(to_numpy)
Time: 0.0003s (0.04%) | Calls: 25
8. ~:0(<method 'to_numpy' of 'builtins.PySeries' objects>)
Time: 0.0003s (0.04%) | Calls: 25
9. ~:0(<method 'copy' of 'numpy.ndarray' objects>)
Time: 0.0002s (0.03%) | Calls: 98
10. ~:0(<method '__array__' of 'numpy.ndarray' objects>)
Time: 0.0002s (0.03%) | Calls: 5
💡 RECOMMENDATIONS:
🎯 Focus on these slow endpoints:
- _dash-update-component: avg 48.1ms, max 731ms
🔧 6 slow Dash callbacks found - consider caching or optimization
I started switching to async & background callbacks but it was even worse so stopped quickly.
Currently my system is designed in a way where a central callback (depictio/depictio/dash/layouts/draggable.py at main · depictio/depictio · GitHub) is updating the dashboard for different types of actions (new component, component edited, component duplicated, interactive action, draggable layout change, …). Each callback update is triggering helper functions (depictio/depictio/dash/component_metadata.py at 71fc3708d24b7ca4a97004dd0d1f592ed7101de8 · depictio/depictio · GitHub), leading to components rebuild in a sequential way. The massive aspect for that callback was initially retained to avoid having duplicate outputs and conflicted callbacks.
If I want to switch to async + background component building, I’m thinking what would be the optimal implementation for this. Would something like below help in optimising the Dash callbacks structure, reducing complexity of the tree and improving performance in general? My idea was to switch from a single callback where build functions are called sequentially to small component-level rendering callbacks using async & background.
@app.callback(
Output("component-render-trigger", "data"),
[Input({"type": "btn-done", "index": ALL}, "n_clicks"),
Input({"type": "btn-done-edit", "index": ALL}, "n_clicks"),
Input({"type": "interactive-component-value", "index": ALL}, "value")],
prevent_initial_call=True
)
def handle_component_interactions(btn_clicks, btn_edit_clicks, values):
"""Light callback - just trigger component updates with action type"""
ctx = dash.callback_context
# Determine action type
if "btn-done-edit" in ctx.triggered[0]["prop_id"]:
action_type = "edit"
elif "btn-done" in ctx.triggered[0]["prop_id"]:
action_type = "new"
elif "interactive-component-value" in ctx.triggered[0]["prop_id"]:
action_type = "update"
else:
action_type = "unknown"
return {
"trigger_id": ctx.triggered_id,
"timestamp": time.time(),
"action": action_type,
"needs_update": True
}
@app.callback(
Output({"type": "draggable-item", "index": MATCH}, "children"),
Input("component-render-trigger", "data"),
State({"type": "component-meta", "index": MATCH}, "data"),
State("local-store", "data"),
background=True,
progress=[Output({"type": "component-progress", "index": MATCH}, "children")]
)
async def render_draggable_component_direct(set_progress, trigger, component_meta, local_data):
if not trigger or not trigger.get("needs_update"):
return dash.no_update
try:
# Set initial progress
set_progress(dmc.Loader(size="sm", variant="dots"))
component_data = await fetch_component_data_async(
component_meta.get("data_collection_id")
)
if component_data:
processed_data = await process_component_data_async(
component_data,
component_meta
)
else:
processed_data = None
component = await build_component_async(
component_meta,
processed_data,
local_data
)
set_progress(None)
return component
except Exception as e:
logger.error(f"Component rendering failed: {e}")
set_progress(None)
return dmc.Alert(
"Component failed to load",
title="Error",
color="red",
icon=DashIconify(icon="mdi:alert")
)
Let me know what’s your feedback and view on this and thanks again for all your help! 
Thomas