-
-
Notifications
You must be signed in to change notification settings - Fork 3.1k
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
CellMeasurer optimizations #341
Comments
Hey @kof, Not sure how I could accommodate cache-by-id, since react-virtualized doesn't know anything about the underlying data (only that there's a collection of N items). Do you have a proposal in mind? Let's chat about it. :) I understand the appeal of managing row-element caching in the base library, but I'm not sure how it would be possible given the HOC approach of |
Row height caching in the user land is bit dirty right now, because there is no param for turning off the index based caching. One needs to call reset methods before every getRowHeight call. |
Element and size caching can be basically done in one caching logic and it is not very hard to do in user land anyways. So probably giving away that responsibility is not bad. |
One more option is to provide a separate HOC for caching, make it based on indexes by default, make this HOC cache elements and heights. User can use such a HOC then as an example to implement any custom caching logic and create own caching HOC's. |
This hints at a problem though. You're implying that your underlying data (the ids) are not tied to index- and that the height varies per id. These 2 things would cause problems with react-virtualized since cell size (for a given index) is important. That being said, react-virtualized is quick to recalculate its layout. The slow part is the rendering of a cell to measure its size. So if you were caching rendered cells (based on id) in user land, and you had a way to map index-to-id, then I'd think you should be able to tell react-virtualized to clear its cache without a significant performance hit (since the next set of measurements would be taken from your user land cache). |
Another reason I shy away from caching the result of render operations is that it's unclear when I should purge the cache, which can lead to memory bloat. I think that's something that varies per-application, so it's another reason to leave it in user land. |
Yep, I agree, it may not fit all use cases and may need different caching strategies. So best thing would be to leave cache out of the library completely or make caching a separate optional HOC. |
I like HOCs (obviously 😄 ) so I like that general idea. Okay, so... I think we're settling on the fact that rendered-cell caching stays in user land (possibly with a HOC to help simplify it). But let's talk more about this row-height issue. Can you explain to me why your data (ids) are shifting around within your collection (indexes)? Also, if you were to be managing rendered-content-caching in user-land, then would that alleviate your performance concerns about You would still need to inform both that the underlying data (and thus the related cell sizes) may have changed though. ( |
My application is a chat with last-to-first message scrolling (reverse order). When I scroll back, index 0 becomes a different message than it was before.
CellMeasurer would still mount the element to the dom and detect height/width on it using DOM properties. This is still dangerous because we never know when they might hit our performance, the only way is to ensure to touch dom as rare as possible.
yep I am currently resetting both
|
Ah, gotcha. Reverse order scrolling. Makes sense. :D Roger that about the mounting/layout hit. Okay. So let's talk about the interface to this cache. Currently, interface CellMeasurerCache {
clearAllColumnWidths(): void;
clearAllRowHeights(): void;
clearColumnWidth(index: Number): void;
clearRowHeight(index: Number): void;
getColumnWidth(index: Number): Number;
getRowHeight(index: Number): Number;
hasColumnWidth(index: Number): Boolean;
hasRowHeight(index: Number): Boolean;
setColumnWidth(index: Number, value: Number): void;
setRowHeight(index: Number, value: Number): void;
} ...then you could manage your own mapping of index-to-id in user land. Thoughts? This interface would support a backwards-compatible release which is important to me. :) I think this change would make |
Do you think we could have a caching interface for both, elements and height? |
For elements it would need to be a component, because it will need to do shallowEqual of data. However maybe there is a away to do it as a separate caching class too. |
In your interface example you have a typo with types. |
I thought we agreed to leave caching elements in user land (for the time being)? Since cache invalidation / expiration is kind of tricky to implement in the lib. |
Although I assume your |
I thought we were talking about both, heights caching and elements caching are equally tricky. So when we are able to create a good interface for both and have an optional class handling it - why not. |
Yes it does it already. I am just trying to simplify the whole thing if it is possible, because the logic for caching heights and elements seems to be mostly the same. |
What if we reduce the caching interface to something minimal and universal for any data? Kinda universal index based caching interface. |
They aren't equally tricky, actually. Caching numbers is a pretty lightweight thing. Caching rendered React elements would cause memory usage to climb much faster. (Basically the "expiration" problem.)
I think this is a reasonable thing to leave in user land. No reason you couldn't cache both with the same object, but I don't think I want to make it an official part of the interface. I don't think it's complex enough to need its own HOC anyway. It would just be a getter function and a map of index (or id, or whatever) to rendered object, right?
I think that would make it a bit harder to work with. For example, if it's a generic caching util, it would have an interface more like this: interface Cache {
clearAll(): void;
clear(id: any): void;
get(id: any): Number;
has(id: any): Boolean;
set(id: any, value: Number): void;
} But at this point you wouldn't be able to use the same object to cache columns and rows. You'd have to have 2 (and a 3rd to cache rendered cells). Seems like this would make things more complicated on end users. |
This is right. However if it is all replaceable, shouldn't be a big deal once it is a problem. If your concern is to cache elements by default in the lib - you could make it optional and false by default.
Yep, you would need a cache instance per use case. I am not sure this is less nicer. The interface itself is nicer, because simpler names and less methods. When user doesn't want to have a custom caching implementation, library creates the cache object automatically, user doesn't need to do anything. In the rare case where user wants to have a custom caching logic like me, he created an own caching class, now he will need to create 2-3 instances of it and pass them to proper component. It looks actually very nice to me, based on imagination). Also user could pass the custom caching class itself to the component and let component instantiate it. |
I still prefer the simplicity of a single cache (mentioned here) for cell sizes. I think it fits better within the rest of the Api. I think it's reasonable to leave rendered-cell-contents entirely in user land. I don't see a need for an explicit interface for this. There's only 1 connection point (the
|
All right then, maybe my suggestions are over engineered, maybe it is enough to just make the heights caching optional in CellMeasurer. |
Didn't mean to suggest they were over-engineered. I think you're focused on an application usage and I'm trying to think generically of library usage. We're just looking at it from different angles. :) I am okay with adding a cache interface for cell sizes. Hopefully that will enable performance wins for your user case. :) |
Just found the real bottleneck, I was measuring performance of the entire _measureCell call, which is pretty bad in my case. I was thinking it is the element creation step, but it turns out it is the renderSubtreeIntoContainer call. renderRow: 0.292ms |
Good to know. Caching heights will help a lot with this then, even if you didn't cache rendered rows. :) |
My problem is even worth, it is too slow on initial render, it spends 20-50ms on every message, after rendering 30, the slowness hurts the ux |
Ah, worse? Hm. Any other ideas? |
Trying to find a way to not just reuse the element, but to reuse the whole mounted thing, so that there is almost no overhead for cell measuring. |
Hm... this cache interface could actually provide an escape hatch to break out of the React ecosystem entirely... if your content is just text, you could use a single hidden/offscreen container to render into and measure... |
So the problem is how to clean up the measured rows if they are not needed for visual rendering? |
Well, that's a problem. The other is how to coordinate this kind of hand-off between |
There might be a way to decouple those 2 also in this approach, will need some grid modifications though. |
I'll admit up front that I'm reluctant to modify |
And I know why)))) Grid code is way too complex.... |
The amount of performance tuning and edge-case handling that's gone into it is more complicated than you'd expect. |
I don't know if I'd call it "too complex". |
I didn't add features or code just for the fun of it. |
Too complex means for me that understanding of that code is very hard. Doesn't mean complexity was invented for fun. |
Haha, gotcha. 😁 No worries. It is also sometimes hard for me to understand if I haven't looked at it in a while. That's why I write so many tests and inline comments. |
From what I can see, modification in Grid would need allow having "ghost" cells, which are visually hidden and are ignored by auto positioning in the grid. Every row can be wrapped into a CellMeasurer HOC. CellMeasurer measures it's size, then tells the grid (not sure how) that the row is measured now and can be used (ghost mode off) Once Grid knows that a cell can be used, it uses it, if grid doesn't need it because (like you say) it's been scrolled away or jumped, grid removes the ghost cell. |
Unfortunately I don't have the bandwidth for any refactoring approaching the scale of what would be required to try out this idea. |
I have been also trying to do the following approach:
There is one problem though ... events handling doesn't work. Didn't find a way to active events handling on manually inserted dom nodes. |
Hm. Afraid I don't have any experience in that area (working with cloned elements) so I'm not of any help. |
Oh of course it won't work, handlers are registered once it's been measured, after that it was unmounted. In the second render, I don't do any createElement calls with handlers ... #selfrealized |
Thanks, @bvaughn and @kof for the discussion in this thread, it has enabled me and my team to get a solution for a very similar problem we were having. I have a notification component that is displayed in reverse chronological order, where each notification has a unique height that is fixed through its lifetime, mounted inside a parent that can filter and reorder the collection. I needed a way to get the cache to work off a unique ID inside the data entity instead of the I figured I would post my solution here since @bvaughn asked for an example in #341 (comment). I broke this out into a wrapper component and a cache and I will give you what the invocation looks like at the callsite too. Here's the first part,
the wrapper component, called
and here's what the callsite looks like
Great work with |
If your company makes some profit, try to convince your boss to become a sponsor or baker. @bvaughn totally earned it. I have tried too, but my company is currently in a shift between seed and A-round and tries to stretch the money until then ... |
Hopefully by the time this gets out and starts making money we can have a little bit to put back into the community that has enabled us to put our system together. React people all do such great work, its a very enjoyable community to watch and work with. |
Oh, and one more thing @bvaughn, most of the actual virtualized code is lifted straight from the wizard on the excellent documentation, but one thing I did notice is that the |
D'oh! Thanks for pointing that out @rexmondo. I'll update the wizard this afternoon. :) I'm glad to hear it's useful for people. Put it together on a whim. Haven't used it myself yet. :D Thanks for the kind words (both of you) and for sharing your approach too. Really cool to see! |
@bvaughn how about using canvas api to measure the pixel size of text (or at least provide an option to the user to specify something like a |
There's nothing preventing you from doing this instead of using |
😃 good point! thanks for a quick response) this project is awesome, btw |
Thank you~ |
Hey, I have seen you did a great job already. I am now about to drop my own custom implementation of the measurer and port everything to the latest react-virtualized.
A few things I noticed are not good enough for my use case:
Row height caching.
Current row height caching is done using the index. However in my case, it is not possible to cache by index, I need a different caching mechanism based on custom id's.
Row element caching.
Currently you create a row element twice, once for measuring, once for representation. I can introduce a caching in the user code, so element creation happens only once, however you might want to solve this in the library.
The text was updated successfully, but these errors were encountered: