Skip to content

Commit

Permalink
Perf: Rely on a single store listener only
Browse files Browse the repository at this point in the history
Currently when a component calls useSelect or any of a number of other hooks,
the store creates a new listener through Redux's subscribe method. In this
patch we're storing a custom list of listeners before calling into Redux and
relying on a single Redux listener to call all of the registered functions.

This reduces the number of comparisons to `getState()` when updtaing the store
and measured a reduction in several key metrics by between 4% to 9%; specifically
the `type`, `focus`, and `inserter` methods.

Co-authored-by: Jarda Snajdr <jsnajdr@gmail.com>
  • Loading branch information
dmsnell and jsnajdr committed Jun 27, 2023
1 parent 69d0790 commit d9c53e1
Show file tree
Hide file tree
Showing 2 changed files with 36 additions and 10 deletions.
40 changes: 30 additions & 10 deletions packages/data/src/redux-store/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import * as metadataSelectors from './metadata/selectors';
import * as metadataActions from './metadata/actions';

/** @typedef {import('../types').DataRegistry} DataRegistry */
/** @typedef {import('../types').ListenerFunction} ListenerFunction */
/**
* @typedef {import('../types').StoreDescriptor<C>} StoreDescriptor
* @template {import('../types').AnyConfig} C
Expand Down Expand Up @@ -158,6 +159,19 @@ export default function createReduxStore( key, options ) {
const storeDescriptor = {
name: key,
instantiate: ( registry ) => {
/**
* Stores listener functions registered with `subscribe()`.
*
* When functions register to listen to store changes with
* `subscribe()` they get added here. Although Redux offers
* its own `subscribe()` function directly, by wrapping the
* subscription in this store instance it's possible to
* optimize checking if the state has changed before calling
* each listener.
*
* @type {Set<ListenerFunction>}
*/
const listeners = new Set();
const reducer = options.reducer;
const thunkArgs = {
registry,
Expand Down Expand Up @@ -290,18 +304,24 @@ export default function createReduxStore( key, options ) {
const subscribe =
store &&
( ( listener ) => {
let lastState = store.__unstableOriginalGetState();
return store.subscribe( () => {
const state = store.__unstableOriginalGetState();
const hasChanged = state !== lastState;
lastState = state;

if ( hasChanged ) {
listener();
}
} );
listeners.add( listener );

return () => listeners.delete( listener );
} );

let lastState = store.__unstableOriginalGetState();
store.subscribe( () => {
const state = store.__unstableOriginalGetState();
const hasChanged = state !== lastState;
lastState = state;

if ( hasChanged ) {
for ( const listener of listeners ) {
listener();
}
}
} );

// This can be simplified to just { subscribe, getSelectors, getActions }
// Once we remove the use function.
return {
Expand Down
6 changes: 6 additions & 0 deletions packages/data/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ export type MapSelect = (

export type SelectFunction = < S >( store: S ) => CurriedSelectorsOf< S >;

/**
* Callback for store's `subscribe()` method that
* runs when the store data has changed.
*/
export type ListenerFunction = () => void;

export type CurriedSelectorsOf< S > = S extends StoreDescriptor<
ReduxStoreConfig< any, any, infer Selectors >
>
Expand Down

0 comments on commit d9c53e1

Please sign in to comment.