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

Avoid reconciliation, alternative component interface #13396

Closed
andrew-aladev opened this issue Aug 14, 2018 · 9 comments
Closed

Avoid reconciliation, alternative component interface #13396

andrew-aladev opened this issue Aug 14, 2018 · 9 comments

Comments

@andrew-aladev
Copy link

Hello. I want to ask a question about a way to avoid reconciliation process.

Today I can see the following process:

  1. Component wants to re-render.
  2. Component render method provides new virtual dom.
  3. Some react diff library tries to find some non-optimal way to morph old virtual dom into new one.

Please fix me if I am wrong, I am not familiar with react codebase.

I can see an information in docs:

you don’t have to worry about exactly what changes on every update

But your solution has complexity about O(n) or even worse, so user should care about what changes sometimes. When user knows what changed he will be able to provide O(log n) or even O(1) solution.

For example I am working with huge data list and I am receiving information from websocket about how to morph my list: append/prepend, remove, swap items, etc. I don't want to render huge component list and run reconciliation process for each mutation. I can tell virtual dom how to morph efficiently.

append

Is there a way for user to provide morph method? I can imagine some api like:

// render is not defined

morph(component) {
  if (...) {
    component.append(<Item />);
  } else {
    (<Item />).prependTo(component.find({ key: '5' }));
  }
}

Do you have any plans to implement it? Thank you. Please feel free to ask any questions.

@gaearon
Copy link
Collaborator

gaearon commented Aug 14, 2018

Before diving deeper — have you considered virtualizing your list (e.g. see https://react-window.now.sh/)? If reconciliation is a bottleneck it's a sign you might be rendering more items than necessary.

@andrew-aladev
Copy link
Author

Hello. I don't have any bottleneck today, but I don't want to receive it tomorrow.

render() { items.map => <item /> }

It has complexity O(n) + reconciliation >= O(n), so complexity will be >= O(n). It couldn't be ok.

createListComponent.js includes render. It is overcomplicated and it still returns an element for reconciliation process. So this process is still in game.

@gaearon
Copy link
Collaborator

gaearon commented Aug 14, 2018

It has complexity O(n) + reconciliation >= O(n), so complexity will be >= O(n). It couldn't be ok.

I think you're oversimplifying this. O(1) vs O(N) doesn't matter in any practical sense if we're talking about something that takes less than a millisecond either way. And if it takes more, N might be large enough that you need to worry about other things — such as the number of DOM nodes and complexity of calculating styles and painting — which often affects performance more significantly than React reconciliation. Reducing this to O(N) notation is missing the crucial details.

The way you juxtapose O(1) and O(N) also doesn't acknowledge differences between deep and shallow reconciliation (which is very significant in practice). React doesn't let you skip the shallow reconciliation but it's a tiny slice of the time it would take to do a deep reconciliation. You can definitely skip the deep one — either through something like PureComponent or by caching React elements. Once you do that, you will likely find that skipping shallow reconciliation doesn't bring you any measurable benefits.

This is why I encourage you to ground this discussion in a practical example. In our experience, if O(N) vs O(1) starts mattering for reconciling e.g. list insertion, you're at a point where you have much more significant ways to optimize your app — either by a windowing technique or by bailing out of reconciling children (or by doing both).

Hope this makes sense.

@andrew-aladev
Copy link
Author

andrew-aladev commented Aug 14, 2018

Sorry, this was just a simple example. render returns a generic subtree of components. It's complexity could be any like O(n*k*...), O(n^k), etc for generic data. Our reconciliation can take much more than 1 millisecond. It's result (append single item or move it) could take comparable amount of time. When it will become an issue we couldn't provide a simple solution. It's a trap.

Let's consider stock exchange. We are managing huge list of stocks. Our complexity is the best available - O(n). Amount of server events are huge. 99% of mutations are price changes, 1% - position changes. Price change leads only to el.textContent =, it will be very fast. Our useless (shallow or deep) reconciliation of all stocks for each price mutation could become an issue. At the same time morph method could be noop for such mutations.

Windowing technique won't be a generic solution. You couldn't select content (ctrl + A hotkey), use content search (ctrl + F hotkey), print page itself, etc and it won't be possible to fix it. All native browser functionality requires all dom elements sittings on their places. It is ok for country/state selection, autocompletes, etc. (when data is not important)

@gaearon
Copy link
Collaborator

gaearon commented Aug 14, 2018

I'd say something like stock exchange is pretty much the worst case you can pick for React — because everything is updating all the time. We don't optimize React for this use case because it’s relatively uncommon (most apps have significantly more “stable” UIs). For such a performance-critical piece it might make sense to completely skip React, and update text nodes manually using refs. You can still use normal React data flow for the rest of your app, but optimize this specific piece separately. But it’s hard to say whether that’s even necessary without a specific example.

When it will become an issue we couldn't provide a simple solution. It's a trap.

Still, this is not a productive way to discuss a hypothetical performance problem. You can build a small stress test that demonstrates how many rows/columns you’re going to have, how many updates you have per second, and how deep the component tree is. I don’t think it would take you more than a few hours to build. You can artificially increase the rendering time for any component to simulate a deeper tree (if that’s what you're worried about). Then we can look at the example and discuss specific fixes you could do to get out of the “trap”. When it’s purely theoretical we can’t consider tradeoffs. In this case, if you never want to be “trapped”, the best way to ensure that is to not use any library.

@NE-SmallTown
Copy link
Contributor

Seems this scenario is a little like the time-slicing demo which dan shows in his talk? But btw, why don't use three.js or d3.js or corresponding React library to do this rather than plain React?

@andrew-aladev
Copy link
Author

Sure, I will provide later a small example. I will investigate react code a bit to provide better performance measurement.

I am wondering why there is only one interface for react component (render method). I am about to be sure it is strongly connected with reconciliation process. I don't want to say that this process is bad and it should be omitted every day.

I saw issues about reconciliation #10382, #10703, etc. I think that react can provide better abstraction around this process and give user a chance to provide custom reconciliation or avoid it sometimes.

@gaearon
Copy link
Collaborator

gaearon commented Aug 16, 2018

Reconciliation is about more than just moving nodes around. Upcoming features like time slicing and suspense require a lot of internal coordination. Exposing even simple hooks to userland can leave us unable to implement some of these features efficiently for the common case.

Ultimately you do have a custom mechanism at hand: use refs if you need to. You can have a component that does everything below imperatively. In fact you can even combine this approach with portals to continue declarative rendering below the parts you're trying to skip.

@necolas
Copy link
Contributor

necolas commented Jan 9, 2020

Closing this as answered

@necolas necolas closed this as completed Jan 9, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants