Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve performance of context components re-rendering #3066

Open
wants to merge 36 commits into
base: dash-3.0
Choose a base branch
from

Conversation

T4rk1n
Copy link
Contributor

@T4rk1n T4rk1n commented Nov 7, 2024

Fix #3057

  • Refactor setProps reducer to partially updates the layout instead of returning a new layout object.
  • New rendering component, DashWrapper replace TreeContainer.
  • Remove DashContext, the state is now handled in react-redux selectors.

Gif showing only the clicked button is re-rendered:
single-update

@T4rk1n
Copy link
Contributor Author

T4rk1n commented Nov 7, 2024

The test_redraw shows failure with 5 redraw instead of 2.

There is two cases of additional redraw that need to be fixed in this pr:

  • the loading_state selector get triggered. Adding +2 redraw.
  • While the props is the same when returning, the equality check fails for props since it's a new object. Adding +1 redraw.
    • Checking all the props all the time is too expensive.
    • Shallow check will fail for objects that might be simple or complex.

{Array.isArray(layout) ? (
layout.map((c, i) =>
isSimpleComponent(c) ? (
c
) : (
<TreeContainer
<DashWrapper
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, new component - is this going to break any legacy code?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it shouldn't as this is an internal component.

@@ -1,4 +1,4 @@
type Config = {
export type DashConfig = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 for the name change - why does it have to be exported?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is used in the new DashWrapper.tsx.

@@ -26,6 +26,18 @@ export const apiRequests = [
'loginRequest'
];

function callbackNum(state = 0, action) {
// With the refactor of TreeContainer to DashWrapper
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you for the explanation

@@ -0,0 +1,425 @@
import React, {useMemo, useCallback} from 'react';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I'm going to trust you on this - I don't know enough of Dash and TypeScript to review something this large. @emilykl can you please have a look? (who else might be a good reviewer?)

@T4rk1n
Copy link
Contributor Author

T4rk1n commented Nov 11, 2024

I think for the additional redraw might be coming from the new path, before it was JSON.stringify(path) and had a memo on that. Think it might need to be back like that or with an additional equalityFn on the memo for the path where it does JSON.stringify to compare the paths.

@NOTMEE12
Copy link

Hi. Are there any updates on this?

@AnnMarieW
Copy link
Collaborator

AnnMarieW commented Dec 9, 2024

@T4rk1n

I ran the dmc-docs using this branch, and it's much faster! Thanks so much for doing this PR. This will make large apps using DMC perform much better.

On the dev branch this app works fine, but on this branch, a component sent to the icon prop throws errors.

import dash_mantine_components as dmc
from dash import Dash, _dash_renderer
_dash_renderer._set_react_version("18.2.0")
from dash_iconify import DashIconify
app = Dash(external_stylesheets=dmc.styles.ALL)



app.layout = dmc.MantineProvider(
    dmc.Checkbox(
            label="Custom checked icon",
            checked=True,
            icon=DashIconify(icon="ion:bag-check-sharp"),
            size="lg",
        )
)

if __name__ == "__main__":
    app.run(debug=True)

Here's one of the error messages (There are a few more as well)

Uncaught TypeError: Cannot read properties of undefined (reading 'props')
    at n.47474.s.default.createElement.r.icon (dash_mantine_components.v0_15_1m1733503561.js:2:1076461)
    at renderWithHooks ([email protected]_18_2m1733760928.2.0.js:16315:20)
    at mountIndeterminateComponent ([email protected]_18_2m1733760928.2.0.js:20084:15)
    at beginWork ([email protected]_18_2m1733760928.2.0.js:21597:18)
    at HTMLUnknownElement.callCallback ([email protected]_18_2m1733760928.2.0.js:4151:16)
    at Object.invokeGuardedCallbackDev ([email protected]_18_2m1733760928.2.0.js:4200:18)
    at invokeGuardedCallback ([email protected]_18_2m1733760928.2.0.js:4264:33)
    at beginWork$1 ([email protected]_18_2m1733760928.2.0.js:27461:9)
    at performUnitOfWork ([email protected]_18_2m1733760928.2.0.js:26567:14)
    at workLoopSync ([email protected]_18_2m1733760928.2.0.js:26476:7)

Update

It's not just the DashIconify library. It's not possible to pass any components to the icon prop:

import dash_mantine_components as dmc
from dash import Dash, _dash_renderer, html
_dash_renderer._set_react_version("18.2.0")

FONT_AWESOME = "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"

app = Dash(external_stylesheets=[FONT_AWESOME])

app.layout = dmc.MantineProvider([
    html.I(className="fa-solid fa-bag-shopping fa-3x"),
    dmc.Checkbox(
            label="Custom checked icon",
            checked=True,
            icon=html.I(className="fa-solid fa-bag-shopping fa-3x"),
            size="lg",
        )
])


if __name__ == "__main__":
    app.run(debug=True)

@T4rk1n
Copy link
Contributor Author

T4rk1n commented Dec 9, 2024

@AnnMarieW I removed the _dashprivate_layout from the given props, it's a "private" props and shouldn't have been used outside of the renderer.

https://github.com/snehilvj/dash-mantine-components/blob/529231820bc2ca25db396cb35d637618b6df62cc/src/ts/components/core/checkbox/Checkbox.tsx#L66

@AnnMarieW
Copy link
Collaborator

AnnMarieW commented Dec 9, 2024

@T4rk1n Thanks for your speedy response. I updated to use React.cloneElement instead and it works fine 🎉

Update: Actually it doesn't work 😢 It doesn't throw errors, but it doesn't render correctly 🤔

@alexcjohnson
Copy link
Collaborator

There's one other user of _dashprivate_layout in this repo (dcc.ConfirmDialogProvider), and it's also used by both DDK and DBE https://github.com/search?q=org%3Aplotly+_dashprivate_layout&type=code, in addition to DBC and as @AnnMarieW points out DMC. @T4rk1n is removing this necessary to the performance improvements? If not, it seems like we really shouldn't tamper with it. But if it is necessary, we'll need to come up with an alternate pattern that will work for our own uses of it, and then give these other libraries time and assistance to adopt the pattern as well.

@T4rk1n
Copy link
Contributor Author

T4rk1n commented Dec 10, 2024

I removed the _dashprivate props because it was not necessary anymore in the wrapper props, still think it was a mistake to include them in given props with a name like that when it should be part of the API. While the performance impact isn't that much to add it back, it does contains the whole layout starting from the component, duplicated for each components the memory footprint is non-negligible for just a few usecases.

The proper way to handle this kind of pattern (send data up/down stream the component tree) in react is with context components, which this PR fixes their performance.

@AnnMarieW
Copy link
Collaborator

@T4rk1n

Thanks for the suggestion of using context instead. We are using the dashprivate props in multiple components in DMC, so this will take a while to update.

You make a good point here:

contains the whole layout starting from the component, duplicated for each components the memory footprint is non-negligible for just a few usecases.

Given that this PR substantially increases performance, would you consider keeping the dashprivate props for now, and removing in a future update?

@T4rk1n
Copy link
Contributor Author

T4rk1n commented Dec 10, 2024

Given that this PR substantially increases performance, would you consider keeping the dashprivate props for now, and removing in a future update?

Yes, I'll put it back since it's used everywhere. There could be an alternative get_props on the clientside that would return the raw props in a future update before deprecating and removing this dashprivate api.

@AnnMarieW
Copy link
Collaborator

This would be awesome:

There could be an alternative get_props on the clientside that would return the raw props in a future update before deprecating and removing this dashprivate api.

❤️ ❤️

@T4rk1n
Copy link
Contributor Author

T4rk1n commented Dec 11, 2024

Not sure I can add back those props in the way it was, there was another component in between the TreeContainer and the library component that is not longer the case. Might need to refactor the new Wrapper to have a middle component but that changes the order of renders and is not as optimal as a single component since it add overhead.

@T4rk1n
Copy link
Contributor Author

T4rk1n commented Dec 11, 2024

And I think now with this to access the child props the new path would be just without a level and the _dashprivate_layout.props.
So instead of child.props._dashprivate_layout.props you would use child.props directly.

@T4rk1n
Copy link
Contributor Author

T4rk1n commented Dec 11, 2024

And I think now with this to access the child props the new path would be just without a level and the _dashprivate_layout.props. So instead of child.props._dashprivate_layout.props you would use child.props directly.

Actually, the props are not there anymore in the wrapper, they are now gotten from the selector, so the old hack cannot work anymore with this solution.

@AnnMarieW
Copy link
Collaborator

so the old hack cannot work anymore with this solution.

Is there a "new hack" that might be easier than using context?

@T4rk1n
Copy link
Contributor Author

T4rk1n commented Dec 11, 2024

so the old hack cannot work anymore with this solution.

Is there a "new hack" that might be easier than using context?

A Context solution might only be valid in some cases, for the mantine checkbox case, it want to add extra props to the component.

I see a couple solution for those cases:

  1. Could add extraProps to DashWrapper props that would be merged with the component props from the store, this would make React.cloneElement(child, {extraProps: {selected: true}}) to work.
  2. change the clientside set_props to accept a path instead of an id. Then you can use children[0]._dashprivate_path to add new props to the components.

For accessing the props (dcc.Tabs cases), a new get_props(path) could be added or refactor the components to use a context.

@insistence
Copy link
Contributor

@AnnMarieW I added all three solutions from the last post.

  1. Added extras prop to DashWrapper component, think this one could replace dmc checkbox:
const iconFunc = ({ indeterminate, ...others }) => {
    const selected: any = indeterminate ? indeterminateIcon : icon;
    return React.cloneElement(selected, {extras: others});
};
  1. set_props can take a dash path instead of id.
  2. Added get_props(pathOrId), to get the props of a component. The path is available in children[0].props.componentPath.

Let me know if that work.

I have a question about the Added extras prop to DashWrapper component. If the third-party React component itself uses React.cloneElement but does not pass the extras property, will the current change not be able to pass additional parameters to child components? Previous related PR #2819 #2831 .

@T4rk1n
Copy link
Contributor Author

T4rk1n commented Jan 10, 2025

@insistence

I have a question about the Added extras prop to DashWrapper component.

I added the extras as an alternative to dashprivate api's, I refactored the test you added a couple months ago yesterday to use the new extras instead and it felt kinda wrong. I think I can do ...restProps instead of the extras prop that would be better so I'll change that.

@T4rk1n T4rk1n changed the base branch from dev to dash-3.0 January 22, 2025 14:55
@T4rk1n
Copy link
Contributor Author

T4rk1n commented Jan 24, 2025

@AnnMarieW This will be ready for merge, I'd like to get the api right before and I am thinking of one last minute change. Putting window.dash_clientside.get_layout(path) on window.dash_component_api.get_layout() instead. This way would be more separated between what should be used in clientside callbacks and inside dash components. What do you think?

@AnnMarieW
Copy link
Collaborator

AnnMarieW commented Jan 24, 2025

Putting window.dash_clientside.get_layout(path) on window.dash_component_api.get_layout() instead. This way would be more separated between what should be used in clientside callbacks and inside dash components.

I agree - it would be more consistent. Is there any use-case for the get_layout being included in dash_clientside ? Would people use it in clientside callbacks?

@@ -73,6 +73,11 @@ const Textarea = ({
);
};

Textarea.dashPersistence = {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the recommended way of setting default for persistence?
It will only work in Dash >=3 ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, only for dash>=3 for support of the persistence props that was previously in defaultProps in functional components and react 18.

Copy link
Collaborator

@AnnMarieW AnnMarieW Jan 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it work in dash< 3 to have the default for persistence set in the function signature? If not, we should check if Dash 3.0 is being use and if not use defaultProps? Is the best way to check for Dash 3.0 by seeing if window.dash_component_api exists?

I also I get an error when trying to set default props in the function signature when the prop is a required prop by Mantine. This might be a separate issue, but do you know how to handle that case?
This is the error, it's like the prop is not getting passed to Mantine, even though it's defined with a default in the function:

"TypeError: Required argument opened was not specified."

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it work in dash< 3 to have the default for persistence set in the function signature?

No it doesn't work since the check is explicit on the defaultProps.

Is the best way to check for Dash 3.0 by seeing if window.dash_component_api exists?

Yes that is the quickest check.

"TypeError: Required argument opened was not specified."

This an error on the Python side of the component, it should have a check if a required prop has a default in the metadata. If you built the components on this branch before I merged 3.0 yesterday might just need to rebuild with the latest changes.

@T4rk1n
Copy link
Contributor Author

T4rk1n commented Jan 24, 2025

Would people use it in clientside callbacks?

I don't think so, it requires the componentPath which is not something the clientside callbacks would really know about.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature something new performance something is slow
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Serious performance issues related to React context
8 participants