-
Notifications
You must be signed in to change notification settings - Fork 2.2k
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
[Table] Eliminate unnecessary re-renders everywhere #998
Conversation
…ecessary re-renders!
Make shallowCompareKeys return false for two arraysPreview: documentation | table |
The table with reordering feature feels super snappy, though it's only 5 rows. We should enable reordering and other fancy features on the full page spreadsheet table, cause this is where the real stress test is. |
packages/table/src/common/utils.ts
Outdated
if (objA[key] !== objB[key]) { | ||
shallowCompareKeys(objA: any, objB: any, keys?: string[]) { | ||
if (keys != null) { | ||
return shallowCompareKeys(objA, objB, keys); |
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.
how is this not an immediate infinite loop every time?
can we please rename the internal function? might be legit to use _
prefix here.
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.
Done
packages/table/src/common/utils.ts
Outdated
* will be compared using the custom `compare` function if one is provided. | ||
*/ | ||
arraysEqual(arrA: any[], arrB: any[], compare?: (a: any, b: any) => boolean) { | ||
if (bothUndefined(arrA, arrB) || bothNull(arrA, arrB)) { |
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 arraysEqual(null, undefined)
should be 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.
Done
packages/table/src/common/utils.ts
Outdated
* Returns true if the arrays are equal. Elements will be shallowly compared by default, or they | ||
* will be compared using the custom `compare` function if one is provided. | ||
*/ | ||
arraysEqual(arrA: any[], arrB: any[], compare?: (a: any, b: any) => boolean) { |
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.
how about compare = (a, b) => a === b
as a default arg here? then line 357 gets real simple
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.
Sweet
packages/table/src/common/utils.ts
Outdated
} | ||
return keys | ||
.map((key) => objA.hasOwnProperty(key) === objB.hasOwnProperty(key) && objA[key] === objB[key]) | ||
.every((isEqual) => isEqual); |
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.
keys.every((key) => objA.hasOwnProperty(key) === objB.hasOwnProperty(key) && objA[key] === objB[key])
please
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.
Whoops, fixed this elsewhere but missed this one
packages/table/src/layers/guides.tsx
Outdated
// array contents explicitly. | ||
const verticalGuidesEqual = Utils.arraysEqual(this.props.verticalGuides, nextProps.verticalGuides); | ||
const horizontalGuidesEqual = Utils.arraysEqual(this.props.horizontalGuides, nextProps.horizontalGuides); | ||
return !verticalGuidesEqual || !horizontalGuidesEqual; |
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.
may be a minor optimization here to inline both vars in the ||
. then if the first one is true it won't compute the second one (i think that's how js works).
return !Utils.arraysEqual(this.props.verticalGuides, nextProps.verticalGuides)
|| !Utils.arraysEqual(this.props.horizontalGuides, nextProps.horizontalGuides);
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.
Noice. That's my understanding too - did this same thing in RegionLayer
.
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 i saw! sounds good
@@ -21,13 +21,26 @@ export interface IRegionLayerProps extends IProps { | |||
regions?: IRegion[]; | |||
|
|||
/** | |||
* A callback interface for applying CSS styles to the regions. | |||
* The array of CSS styles to apply to each region. |
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.
how do we know which styles belong to which region? just by index? worth noting in docs here i think.
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.
Done. LMK if the wording needs to be massaged though.
public shouldComponentUpdate(nextProps: IRegionLayerProps) { | ||
// shallowly comparable props like "className" tend not to change in the default table | ||
// implementation, so do that check last with hope that we return earlier and avoid it | ||
// altogether. |
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.
👍
@@ -528,6 +528,11 @@ export class Regions { | |||
return regionGroups; | |||
} | |||
|
|||
public static regionsEqual(regionA: IRegion, regionB: IRegion) { |
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.
why move this? the previous location seems more logical to me: next to all the other comparison checks.
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.
It's public now, so it has to move up.
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.
ohhh public gotcha ok
const { viewportRect } = this.state; | ||
if (viewportRect == null) { | ||
return {}; | ||
} |
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 viewportRect
check seems to happen in every one of these functions. maybe it can be moved up a level to the function that calls these style*Region()
functions?
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.
Played with this. Looks like viewportRect
is not considered in styleBodyRegion
, so we can't fully generalize. Will leave as is for now.
packages/table/test/utilsTests.ts
Outdated
runTest(false, [3], []); | ||
runTest(false, [3, 1, 2], [3, 1]); | ||
runTest(false, [{}], [{}]); | ||
runTest(false, [{ x: 1 }], [{ x: 1 }]); |
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.
changes in this file generally feel like too much testing. most of these cases are equivalent: they're different instances of the same thing so of course they're not shallow equal. []
is the same as [3]
, from my perspective.
i would do this with like 1-3 cases in each describe
but not worry about testing every possible permutation.
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.
Done
Oops, undo describe.onlyPreview: documentation | table |
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.
clicking and dragging feels faster, but scrolling feels about the same
/** | ||
* Partial shallow comparison between objects using the given list of keys. | ||
*/ | ||
function _shallowCompareKeys(objA: any, objB: any, keys: string[]) { |
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.
any way you can simplify this to all just one function?
shallowCompareKeys: function shallowCompareKeys(objA: any, objB: any, keys?: string[]) {
...
// recurse
shallowCompareKeys(...);
}
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.
Would literally be the second time I've actually had to use recursion in the wild, ha. Let me dust off my knowledge and give it a try.
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.
recursion is a tree's best friend
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.
Except for this kid. https://www.youtube.com/watch?v=1TZCP6OqRlE
@adidahiya ended up pulling the duplicate validation checks into one place and greatly simplifying the helper function. Didn't do the recursive approach though. This look okay now?
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.
Looks good to me, though agree with Adi that scrolling seems really really slow on the formats table. Wish we knew why...
@gscshoyru I definitely see the symptoms in the code. Will spend today getting to the bottom of it. (If you're in a masochistic mood, you should try clicking a column header in that formats table with |
Reduce duplicate logic in shallowCompareKeysPreview: documentation | table |
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 refactor
Checklist
Redux of #988 that fixes some of the nefarious bugs that popped up.
Changes proposed in this pull request:
Goal: Improve table perf by eliminating unnecessary re-renders, particularly when clicking+dragging.
@PureRender
decorator throughoutpackages/table
.shouldComponentUpdate
toGuideLayer
to avoid unnecessary re-renders.shouldComponentUpdate
toRegionLayer
to avoid unnecessary re-renders.RegionLayer
props to accept pre-computedregionStyles
instead ofgetRegionStyle
function. Before, we were creating this styling function inline intable.tsx
on each render, causing shallow comparison betweenthis.props.getRegionStyle === nextProps.regionStyle
to always returnfalse
inRegionLayer.shouldComponentUpdate
. Ultimately, this led to tons of unnecessary re-renders of the region layer (e.g. as you were clicking + dragging). LMK if you want to know more.Utils.shallowCompareKeys
(+ tests) to compare entire objects, not just subset of keys.Utils.arraysEqual
(+ tests) to compare arrays using an optionalcompare
callback.Reviewers should focus on:
shouldComponentUpdate
is tricky to get right.