-
Notifications
You must be signed in to change notification settings - Fork 4.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
Unmemoize Block component selectors #58355
Conversation
Size Change: -9 B (0%) Total Size: 1.7 MB
ℹ️ View Unchanged
|
Flaky tests detected in 73e3c78. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/7698771238
|
const block = useMemo( | ||
() => ( { ...blockWithoutAttributes, attributes } ), | ||
[ blockWithoutAttributes, attributes ] | ||
); |
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.
The reason this is better than selector cache is because it's stored by component instance, not globally.
2554b5f
to
c06dc6d
Compare
c06dc6d
to
e00b44b
Compare
@@ -578,7 +583,7 @@ function BlockListBlockProvider( props ) { | |||
mayDisplayControls: | |||
_isSelected || | |||
( isFirstMultiSelectedBlock( clientId ) && | |||
getMultiSelectedBlockClientIds().every( | |||
getSelectedBlockClientIdsUnmemoized().every( |
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.
Maybe mayDisplayControls
etc. should be selectors instead, but it's private anyway so it matters little.
|
||
return blockOrder.slice( startIndex, endIndex + 1 ); | ||
}, | ||
getSelectedBlockClientIdsUnmemoized, | ||
( state ) => [ | ||
state.blocks.order, | ||
state.selection.selectionStart.clientId, |
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 don't fully understand why unmemoizing getSelectedBlockClientIds
in particular should be faster. You write:
createSelector
needs to keep a cache of all previous selector calls by arguments and then constantly look up cache for new calls. So if you have a 1000 blocks, that’s a huge loop of checks
This would be true if the selector had a clientId
argument. Then the cache would contain a long linked list of memoized result values for each clientId
argument value. But for this selector, the cache is a rather small structure:
- At the top there is a
WeakMap
keyed bystate.blocks.order
. It should have just one record most of the time, because the oldorder
array value are forgotten when the reducer state changes. - The value of the
WeakMap<Order>
record is anotherWeakMap
, keyed by a specialLEAF_KEY
object, indicating there are no more object-like dependant values. - The value of the
WeakMap<LeafKey>
record is a linked list that always has just one record, for the[]
argument array, because the selector has no arguments. - This one
[]
record contains the memoized return value. - The
[]
record is cleared when the currentclientId
is different from the lastclientId
. In other words,rememo
remembers only the result for the lastclientId
used.
To summarize, there is just one three-level cache.get( order ).get( LEAF_KEY ).find( r => r.clientId === clientId )
lookup in structures that have only one member. For reference, look at rememo
source and how we have the isUniqueByDependants === false
case for string/number clientId
.
So, I think that unmemoizing this particular selector is controversial: it returns an array that deserves to have stable references, and I don't see where the speedup would come from.
For other selectors that return boolean values, I think unmemoization makes sense. Especially the ones that have arguments. It it plausible that recalculating the result from scratch is faster than maintaining large memoization maps.
One other such selector that comes to mind is canInsertBlockType
. I've been changing it in #58262.
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.
Then the cache would contain a long linked list of memoized result values for each clientId argument value.
Yes, that's true
); | ||
}; | ||
return getBlockOrder( state, clientId ).every( isChildSubtreeDisabled ); | ||
}; |
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.
After #58349, this is no longer called, so the performance improvement of this PR will be lessened.
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.
Probably still worth to remove the memoization I think... Also the dependencies of this selector weren't fully correct.
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.
Also, this selector should also check the editing mode of the top-level clientId
, not just its children.
If I look at the only call in BlockListBlockProvider
, I see that it's done anyway, checking blockEditingMode === 'disabled' && isBlockSubtreeDisabled()
.
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 like the getBlockWithoutAttributes
idea 🙂 There's however the React Native version of this code that still uses __unstableGetBlockWithoutInnerBlocks
and deserves not to be left behind.
@@ -432,7 +435,7 @@ const applyWithSelect = withSelect( ( select, { clientId, rootClientId } ) => { | |||
// This function should never be called when a block is not present in | |||
// the state. It happens now because the order in withSelect rendering | |||
// is not correct. | |||
const { name, attributes, isValid } = block || {}; |
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.
For mobile, there's not really a need to keep passing attributes in the block object because mobile is not extensible by third party plugins.
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 don't understand this because I see that the wrapped BlockListBlock
component uses the attributes
prop several times. For example, when calling getAccessibilityLabel
. And that uses, for example, attributes.level
to say "this is a level 2 heading".
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.
attributes
yes, but not block.attributes
?
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 see, you're assigning the attributes
variable elsewhere, with getBlockAttributes
:) Nevermind.
I can't see any improvement in the numbers anymore. Maybe because of continuously calling |
What?
When there's 1000s of blocks, selector cache lookups may be expensive. This selector cache is relatively useless, unless the final result is non primitive derived state. I think doing it otherwise is harmful because createSelector needs to keep a cache of all previous selector calls by arguments and then constantly look up cache for new calls. So if you have a 1000 blocks, that’s a huge loop of checks (for what otherwise could have been a simpler state calculation).
Why?
Update: the performance impact is now lessened because of #58349.
Testing Instructions
Testing Instructions for Keyboard
Screenshots or screencast