Skip to content

Performance regression in dash 3.0 #3245

Closed

Description

@tlauli

Describe your context
Please provide us your environment, so we can easily reproduce the issue.

  • replace the result of pip list | grep dash below
    New env
dash                                     3.0.0
dash_ag_grid                             31.3.1
dash_auth                                2.3.0
dash-bootstrap-components                2.0.0
dash-bootstrap-templates                 2.1.0
dash-pivottable                          0.0.2

Old env

dash                                     2.18.2
dash_ag_grid                             31.2.0
dash_auth                                2.3.0
dash-bootstrap-components                1.7.1
dash-bootstrap-templates                 1.3.0
dash-core-components                     2.0.0
dash-html-components                     2.0.0
dash-pivottable                          0.0.2
dash-table                               5.0.0

  • if frontend related, tell us your Browser, Version and OS

    • OS: iOS
    • Browser: Chrome
    • Version : 134.0.6998.118

Describe the bug

Page that used to work without any issues is now extremely laggy.

Expected behavior

No performance decrease in the new version.

I tried profiling it in react dev tools, but I do not really have a good idea what I am looking at.

Activity

  1. tlauli commented on Mar 25, 2025

    @tlauli
    ContributorAuthor

    The profiles are here. Zipped because the jsons were refusing to upload properly for some reason. Profiles are with dev tools enables, but the same situation happens when deployed with dev tools disabled. (replaced with MWE)

  2. tlauli commented on Mar 26, 2025

    @tlauli
    ContributorAuthor

    This MWE (not really minimal, but I hope this is simple enough) replicates the issue. On my M1 macbook pro, with dash 2.18 the page works without issue, tooltips are responsive as expected. On dash 3.0, the page is extremely laggy, tooltips are taking a second or two to appear and then often failing to disappear. I tried setting dash._dash_renderer._set_react_version("18.2.0") on dash 2.18, and it still worked ok, so I expect the problem is somewhere in the new version of dash.

    from random import random
    
    import dash_bootstrap_components as dbc
    from dash import Dash, Input, Output, callback, dcc, html
    
    APP_NAME = "mwe"
    
    row_num = 8
    
    
    def get_div_with_tooltip(id_prefix: str, variable: str, rc: str, inout: str, tooltip_title: str) -> dbc.Col:
        div_id = {"app_name": id_prefix, "component": "div", "var": variable, "rc": rc, "inout": inout}
        tool_tip_id = {**div_id, "component": "div_tooltip"}
        title = f"{tooltip_title}\n{rc} ({rc}): no data"
        div = html.Div("no data", id=div_id)
        tooltip = dbc.Tooltip(children=title, id=tool_tip_id, target=div_id, trigger="hover", placement="bottom", style={"white-space": "pre"})
        div_col = dbc.Col([div, tooltip], width="auto")
        return div_col
    
    
    def get_spread_div_with_tooltip(id_prefix: str, variable: str, rc: str, title_prefix: str, use_bold: bool) -> dbc.Col:
        div_id = {"app_name": id_prefix, "component": "div", "var": variable, "rc": rc, "inout": "rc"}
        tool_tip_id = {**div_id, "component": "div_tooltip"}
        title = f"{title_prefix}\n\nin\n\nout"
        div = html.H5(html.B("no data", id=div_id)) if use_bold else html.Div("no data", id=div_id)
        tooltip = dbc.Tooltip(children=title, id=tool_tip_id, target=div_id, trigger="hover", placement="bottom", style={"white-space": "pre"})
        div_col = dbc.Col([div, tooltip], width="auto")
        return div_col
    
    
    def get_card_title(id_prefix: str, rc: str) -> html.Div:
        title_id = {"app_name": id_prefix, "component": "div", "var": "title_isos", "rc": rc, "inout": "rc"}
        title_b = html.B(rc, id=title_id)
        f_title = html.B("F: \n")
        f_tooltip_text = "test"
        f_tooltip = dbc.Tooltip(
            children=[f_title, f_tooltip_text], target=title_id, trigger="hover", placement="bottom", style={"white-space": "pre"}
        )
        icon_table = html.I(className="bi bi-table", style={"color": "rgba(0, 0, 0, 0.5)", "padding": "0", "display": "inline-flex"})
        icon_button_id = {"app_name": id_prefix, "component": "button", "var": "table-icon", "rc": rc}
        table_button = dbc.Button(children=[icon_table], id=icon_button_id, style={"backgroundColor": "transparent", "border": "0px"})
        title_row = dbc.Row(
            [
                dbc.Col([], align="start", width="auto"),
                dbc.Col([title_b, f_tooltip], align="center", width="auto"),
                dbc.Col(table_button, align="end", width="auto"),
            ],
            justify="between",
        )
        card_title = html.Div(
            [title_row],
            className="card-title my-0",
        )
        return card_title
    
    
    def get_single_card(id_prefix: str, rc: str) -> dbc.Card | str:
        x_in = get_div_with_tooltip(id_prefix, "d", rc, "in", "XXX")
        x_out = get_div_with_tooltip(id_prefix, "d", rc, "out", "XXX")
        x_avg_in = get_div_with_tooltip(id_prefix, "r", rc, "in", "XXX")
        x_avg_out = get_div_with_tooltip(id_prefix, "r", rc, "out", "XXX")
        x_s = get_spread_div_with_tooltip(id_prefix, "5", rc, "XXX", True)
        y_s = get_spread_div_with_tooltip(id_prefix, "o", rc, "XXX", False)
        card_title = get_card_title(id_prefix, rc)
        card_body = html.Div(
            [
                dbc.Row([x_in, x_out], justify="between"),
                dbc.Row([x_s], justify="around"),
                dbc.Row([x_avg_in, y_s, x_avg_out], justify="between"),
            ],
            className="card-text",
        )
        card_line = html.Hr(style={"margin-top": "5px"})
        card_id = {"app_name": id_prefix, "component": "card", "var": "color", "rc": rc, "inout": "rc"}
        card = dbc.Card([card_title, card_line, card_body], body=True, color="#FFFFFF", className="my-2", id=card_id)
        return card
    
    
    layout = dbc.Container(
        [
            *(dbc.Row([dbc.Col(get_single_card(APP_NAME, f"{r}_{c}")) for c in range(5)]) for r in range(row_num)),
            dcc.Interval(
                id={"app_name": APP_NAME, "id": "interval"},
                interval=1000,
                n_intervals=0,
            ),
        ],
        # fluid=True,
    )
    
    
    @callback(
        output={
            f"{r}_{c}": [
                Output({"app_name": APP_NAME, "component": "div", "var": "5", "rc": f"{r}_{c}", "inout": "rc"}, "children"),
                Output(
                    {"app_name": APP_NAME, "component": "div_tooltip", "var": "5", "rc": f"{r}_{c}", "inout": "rc"},
                    "children",
                ),
                Output({"app_name": APP_NAME, "component": "card", "var": "color", "rc": f"{r}_{c}", "inout": "rc"}, "color"),
                Output({"app_name": APP_NAME, "component": "div", "var": "5", "rc": f"{r}_{c}", "inout": "rc"}, "style"),
            ]
            for r in range(row_num)
            for c in range(5)
        },
        inputs=[
            Input({"app_name": APP_NAME, "id": "interval"}, "n_intervals"),
        ],
    )
    def update1(_) -> dict[str, tuple[str, str, str, dict]]:
        data = {f"{r}_{c}": (f"{random() :.2f}", f"{random() :.2f}", "rgba(0, 0, 0, 0.5)", {}) for r in range(row_num) for c in range(5)}
        return data
    
    
    @callback(
        output={
            f"{r}_{c}": [
                Output({"app_name": APP_NAME, "component": "div", "var": "o", "rc": f"{r}_{c}", "inout": "rc"}, "children"),
                Output(
                    {"app_name": APP_NAME, "component": "div_tooltip", "var": "o", "rc": f"{r}_{c}", "inout": "rc"},
                    "children",
                ),
                Output({"app_name": APP_NAME, "component": "div", "var": "o", "rc": f"{r}_{c}", "inout": "rc"}, "style"),
            ]
            for r in range(row_num)
            for c in range(5)
        },
        inputs=[
            Input({"app_name": APP_NAME, "id": "interval"}, "n_intervals"),
        ],
    )
    def update2(_) -> dict[str, tuple[str, str, dict]]:
        data = {f"{r}_{c}": (f"{random() :.2f}", f"{random() :.2f}", {}) for r in range(row_num) for c in range(5)}
        return data
    
    
    @callback(
        output={
            (f"{r}_{c}", inout): [
                Output({"app_name": APP_NAME, "component": "div", "var": "r", "rc": f"{r}_{c}", "inout": inout}, "children"),
                Output(
                    {"app_name": APP_NAME, "component": "div_tooltip", "var": "r", "rc": f"{r}_{c}", "inout": inout},
                    "children",
                ),
            ]
            for r in range(row_num)
            for c in range(5)
            for inout in ["in", "out"]
        },
        inputs=[
            Input({"app_name": APP_NAME, "id": "interval"}, "n_intervals"),
        ],
    )
    def update3(_) -> dict[tuple[str, str], tuple[str, str]]:
        data = {(f"{r}_{c}", inout): (f"{random() :.2f}", f"{random() :.2f}") for r in range(row_num) for c in range(5) for inout in ["in", "out"]}
        return data
    
    
    app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
    app.layout = layout
    app.run()
    
  3. ndrezn commented on Mar 26, 2025

    @ndrezn
    Contributor

    Hey @tlauli, I do see a difference in performance for Dash=3 and Dash<3 when loading tooltips. I wonder if this is related to #3066 which changes how components are rendered & updated. @T4rk1n would be able to give a better read here.

  4. BSd3v commented on Mar 31, 2025

    @BSd3v
    Contributor

    Hey @tlauli,

    I believe that my PR may address this: #3248

  5. added
    bugsomething broken
    P1needed for current cycle
    on Mar 31, 2025
  6. changed the title [-][BUG] Performance regression in dash 3.0[/-] [+]Performance regression in dash 3.0[/+] on Mar 31, 2025
  7. tlauli commented on Apr 1, 2025

    @tlauli
    ContributorAuthor

    @BSd3v I tried your branch, the performance is better than in 3.0, but still noticeably worse than in 2.18. The tooltips still feel laggy and are failing to hide pretty often.

  8. BSd3v commented on Apr 1, 2025

    @BSd3v
    Contributor

    This is due to the difference in how Dash 3 renders vs 2.

    2 renders the whole app for each component change, the more components you are adjusting at the same time, the more efficient this becomes.

    3 renders the individual components and children, if their props were altered. This is more performant when not modifying a massive amount of components at a single time.

  9. BSd3v commented on Apr 1, 2025

    @BSd3v
    Contributor

    As a note, the tooltip is querying the context of the loading of the target component, this is done in a different way. And might be better to determine if the component is mounted vs loading. This change would have to be done in DBC.

  10. tlauli commented on Apr 1, 2025

    @tlauli
    ContributorAuthor

    This is due to the difference in how Dash 3 renders vs 2.

    2 renders the whole app for each component change, the more components you are adjusting at the same time, the more efficient this becomes.

    3 renders the individual components and children, if their props were altered. This is more performant when not modifying a massive amount of components at a single time.

    Ok, this is a bit unfortunate for me. But it makes sense to not render the whole app when a single prop changes.

    Would it be possible to somehow limit the number of renders? Rendering the parent instead of most of its children? Preventing the render if the prop is adjusted to the same value it was?

    As for the current situation, I think I can work around the perf issue by sending no_updates for the values that stayed the same (which is most of the time all of them), if I understand this correctly. But it would be nice not having to hack it in user code.

  11. BSd3v commented on Apr 1, 2025

    @BSd3v
    Contributor

    If you refactor the code to update the whole card at a single time, it may help.

    Or even using a patch update on the containing div. I know its more tricky to navigate from the app build.

    My PR originally tested for component props being the same and not doing anything, however, there are sometimes that the props dont change and the app needed to rerender the component. There are components that count how many times they render, even though their props stay the same, they needed to rerender.

    Anyways, I think refactoring might work, that way its rendering from the top down, instead of each individual component. 😄

  12. tlauli commented on Apr 1, 2025

    @tlauli
    ContributorAuthor

    Yeah, we did it this way because there are 5 types of data in a single card, and the data is partitioned by these types, instead of the card it belongs to. Also I think the Tooltip disappears when the components it belongs to is replaced in the DOM under the mouse pointer, which is really annoying to users, but I may be confusing this with something else.

    With the improvements in the linked PR I think we can refactor our code so the page runs acceptably, so thanks for the optimizations and pointing out what we should improve in our code. ❤

  13. T4rk1n commented on Apr 15, 2025

    @T4rk1n
    Contributor

    Look like the code can now run acceptably, there might still be improvement to be made to the system but this is mostly resolved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1needed for current cyclebugsomething brokenperformancesomething is slowregressionthis used to work

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions