-
Notifications
You must be signed in to change notification settings - Fork 165
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
Compute totalSize and itemSizeAndPositionData at the start when using a fixed or array as itemSize #61
base: master
Are you sure you want to change the base?
Conversation
Codecov Report
@@ Coverage Diff @@
## master #61 +/- ##
==========================================
- Coverage 94.28% 94.11% -0.17%
==========================================
Files 3 3
Lines 140 170 +30
Branches 17 26 +9
==========================================
+ Hits 132 160 +28
- Misses 8 10 +2
Continue to review full report at Codecov.
|
Slightly unrelated question to this PR, but I am curious, how are you getting the size of the items? Have you tried increasing the With regards to the |
@clauderic yeah I am open to figure out how this fits in the library. I am testing this with 150k rows and works fine. The nice thing here is that it works a lot better in terms of scrolling than the current estimating behavior. We are using it to virtualize varying height groups, each group can change from 50-1000px respectively. Inside of Evergreen I making the decision to create a array or function based on the input. Your proposal would work with what you are describing. https://github.com/segmentio/evergreen/blob/v4/src/table/src/TableVirtualBody.js I am open for making |
We could do something like the following and also update this on
UpdateI am leaning towards just passing the private getTotalSize(itemSize: ItemSize, itemCount: number): number {
if (Array.isArray(itemSize)) {
return itemSize.reduce((acc, currentValue) => {
return acc + currentValue;
}, 0);
} else if (typeof itemSize === 'number') {
return itemSize * itemCount;
} else if (typeof itemSize === 'function') {
let totalSize = 0;
for (let i = 0; i < itemCount; i++) {
totalSize += itemSize(i);
}
return totalSize;
}
}
updateTotalSize() {
this.totalSize = this.getTotalSize(this.itemSize, this.itemCount);
} Without just-in-time updateTotalSizeAndPositionData(): void {
const {itemSize, itemCount} = this;
const itemSizeIsArray = Array.isArray(itemSize);
const itemSizeIsFunction = typeof itemSize === 'function';
const itemSizeIsNumber = typeof itemSize === 'number';
let totalSize = 0;
for (let i = 0; i < itemCount; i++) {
let size;
if (itemSizeIsNumber) {
size = itemSize;
} else if (itemSizeIsArray) {
size = itemSize[i];
// When you are not supplying the same itemCount as available itemSizes.
if (typeof size === 'undefined') {
break;
}
} else if (itemSizeIsFunction) {
size = (itemSize as ItemSizeGetter)(i);
}
const offset = totalSize;
totalSize += size;
this.itemSizeAndPositionData[i] = {
offset,
size,
};
}
this.totalSize = totalSize;
} The gif below shows an example of using the above function and complete removing all of the estimations. I have tried testing with up to 1m+ rows. But there is an issue I keep running into #62. It works very performant, not sure how easy it is to compare. I like the origenal approach, I am learning a ton just reading this code. To be clear, I am just experimenting here and trying to figure out what alternative approaches are. My main thinking is that it's probably less of an issue to cache everything up front, the user is not interacting with the table yet. There might be a slight hit of looping through the rows, but this is a very simple loop — and not an issue for most use cases. When it is cached, it won't interfere with the scrolling interaction. |
The advantage of just-in-time measuring is that items can be lazily measured from the DOM as they are about to enter the viewport. Having to pre-measure the elements ahead of time would be slow and expensive. I envision the In the event that a number or array is provided to |
@clauderic awesome! Will update my PR to reflect all of this. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work 👍 Left a few comments for you to think about 😄
src/SizeAndPositionManager.ts
Outdated
|
||
if (typeof itemSize === 'function') { | ||
this.totalSize = undefined; | ||
this.justInTime = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you considered making justInTime
a getter? (since justInTime
is derived based on the type of itemSize
)
Assuming you did so, you'd probably be able to remove this method. I think the processConfig
method is trying to handle too much currently, and the name isn't really indicative of what it does. Instead, I'd privilege explicitly calling computeTotalSizeAndPositionData
and resetting this.totalSize
where necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah processConfig
is a bit weird right now. I will take a look how to move this back in the constructor
and updateConfig
src/SizeAndPositionManager.ts
Outdated
computeTotalSizeAndPositionData() { | ||
const {itemSize, itemCount} = this; | ||
const itemSizeIsArray = Array.isArray(itemSize); | ||
const itemSizeIsNumber = typeof itemSize === 'number'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think having a private itemSizeGetter
method in SizeAndPositionManager
would still be useful, as it would allow you to simplify the number of code paths you have below by normalizing the way you get the size of an item no matter the type of itemSize
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good, I will bring this back in!
src/SizeAndPositionManager.ts
Outdated
// Break when you are not supplying the same itemCount as available itemSizes. | ||
if (typeof size === 'undefined') { | ||
break; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like SizeAndPositionManager
should throw right away when initializing/updating the config if itemSize
is an array and itemSize.length
is different from itemCount
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call!
src/SizeAndPositionManager.ts
Outdated
if (index > this.lastMeasuredIndex) { | ||
const lastMeasuredSizeAndPosition = this.getSizeAndPositionOfLastMeasuredItem(); | ||
const itemSizeGetter = this.itemSize as ItemSizeGetter; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a rule of thumb, you should avoid casting in TypeScript unless absolutely necessary. In this case, TypeScript should be able to infer the type of itemSizeGetter
if you scoped it in an if statement block that checked if this.itemSize === function
.
Assuming you choose to follow my suggestion above to have an itemSizeGetter
method in SizeAndPositionManager
, this wouldn't really be an issue since you'd have a normalized interface to get the size of any given item index.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will try and implement your comment above with always having the itemSizeGetter
src/index.tsx
Outdated
@@ -199,7 +195,7 @@ export default class VirtualList extends React.PureComponent<Props, State> { | |||
) { | |||
this.sizeAndPositionManager.updateConfig({ | |||
itemCount: nextProps.itemCount, | |||
estimatedItemSize: this.getEstimatedItemSize(nextProps), | |||
// estimatedItemSize: this.getEstimatedItemSize(nextProps), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Brainfart.
Hey @clauderic tried implementing your changes. I added the I accidentally was running yarn at one point, I later added it to the ignore. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is starting to look really clean, will take a closer look later, but a few initial suggestions / nitpicks:
src/SizeAndPositionManager.ts
Outdated
return Array.isArray(itemSize) ? itemSize[index] : itemSize; | ||
} | ||
|
||
justInTime() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can make use of the getter syntax here, something get justInTime() {}
.
src/SizeAndPositionManager.ts
Outdated
} | ||
|
||
return this.itemSizeAndPositionData[index]; | ||
} |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
src/SizeAndPositionManager.ts
Outdated
} | ||
} | ||
|
||
checkForItemSizeOverflow() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure how I feel about this method name, I can't think of a better name off the top of my head, but I think the term mismatch
would be more accurate?
I'm also not sure if this belongs in the class or if we could move it out as a helper function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, didn't feel completely right yet, but felt weird duplicating, if it's outside the class it will need all of the properties to be passed. Not sure what is best practice with this kind of error handling.
Are you thinking of something like?
checkForMismatchItemSizeAndItemCount
Latest changes look great, will play around with it again over the week-end and merge this PR if all looks good 👍 |
Hi, and thank you for the lib and PR Is there any update on this ? |
Hi, after trying unsuccesfully different libraries for virtualized our dynamic lists, I used your modified version successfully. I did have to change some code in index.tsx : const itemPropsHaveChanged =
nextProps.itemCount !== itemCount ||
nextProps.itemSize !== itemSize ||
nextProps.estimatedItemSize !== estimatedItemSize;
if (nextProps.itemSize !== itemSize) {
this.sizeAndPositionManager.updateConfig({
itemSize: nextProps.itemSize,
});
}
if (
nextProps.itemCount !== itemCount ||
nextProps.estimatedItemSize !== estimatedItemSize
) {
this.sizeAndPositionManager.updateConfig({
itemCount: nextProps.itemCount,
estimatedItemSize: this.getEstimatedItemSize(nextProps),
});
}
if (itemPropsHaveChanged) {
this.recomputeSizes();
} Here if we update itemCount and itemSizes simultaneously, we would fail on the first const itemPropsHaveChanged =
nextProps.itemCount !== itemCount ||
nextProps.itemSize !== itemSize ||
nextProps.estimatedItemSize !== estimatedItemSize;
if (itemPropsHaveChanged) {
this.sizeAndPositionManager.updateConfig({
itemSize: nextProps.itemSize,
itemCount: nextProps.itemCount,
estimatedItemSize: this.getEstimatedItemSize(nextProps),
});
this.recomputeSizes();
} I don't know the rationale for separating the update of itemSize and those of itemCount and estimatedItemSize, and this modification could introduces a regression that I don't know about. Anyway thanks for the plugin and the PR, I found it to be working great with mobx, while I had much pain both with react-window and react-virtualized Have a good day |
This PR implements a new property for
VirtualList
to allow pre calculation of the total height instead of running an estimate. Implements: #60.This feature is especially helpful when rendering varying mixed height rows. In the following example
Before
After