ClaimTilesDiscover: optimize props to reduce renders

Props: either use primitives, stable references or memoized objects to reduce renders. This update will simplify the existing `areEqual`. It is still needed though as some props are hard to memoize from where they are called, or should simply be ignore.
This commit is contained in:
infinite-persistence 2021-12-29 14:22:55 +08:00 committed by Thomas Zarebczan
parent b1f4a2a590
commit b0bea66b1d
3 changed files with 70 additions and 69 deletions

View file

@ -1,3 +1,4 @@
// @flow
import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import { selectClaimSearchByQuery, selectFetchingClaimSearchByQuery, selectClaimsByUri } from 'redux/selectors/claims';
@ -9,6 +10,7 @@ import { selectClientSetting, selectShowMatureContent } from 'redux/selectors/se
import { selectMutedAndBlockedChannelIds } from 'redux/selectors/blocked';
import { ENABLE_NO_SOURCE_CLAIMS, SIMPLE_SITE } from 'config';
import * as CS from 'constants/claim_search';
import { createNormalizedClaimSearchKey } from 'util/claim';
import ClaimListDiscover from './view';
@ -17,13 +19,18 @@ const select = (state, props) => {
const hideReposts = selectClientSetting(state, SETTINGS.HIDE_REPOSTS);
const mutedAndBlockedChannelIds = selectMutedAndBlockedChannelIds(state);
// TODO: memoize these 2 function calls. Lots of params, though; might not be feasible.
const options = resolveSearchOptions({ showNsfw, hideReposts, mutedAndBlockedChannelIds, pageSize: 8, ...props });
const searchKey = createNormalizedClaimSearchKey(options);
return {
claimSearchByQuery: selectClaimSearchByQuery(state),
claimSearchResults: selectClaimSearchByQuery(state)[searchKey],
claimsByUri: selectClaimsByUri(state),
fetchingClaimSearchByQuery: selectFetchingClaimSearchByQuery(state),
fetchingClaimSearch: selectFetchingClaimSearchByQuery(state)[searchKey],
showNsfw,
hideReposts,
options: resolveSearchOptions({ showNsfw, hideReposts, mutedAndBlockedChannelIds, pageSize: 8, ...props }),
// Don't use the query from 'createNormalizedClaimSearchKey(options)' since that doesn't include page & release_time
optionsStringified: JSON.stringify(options),
};
};
@ -37,6 +44,27 @@ export default withRouter(connect(select, perform)(ClaimListDiscover));
// ****************************************************************************
// ****************************************************************************
type SearchOptions = {
page_size: number,
page: number,
no_totals: boolean,
any_tags: Array<string>,
channel_ids: Array<string>,
claim_ids?: Array<string>,
not_channel_ids: Array<string>,
not_tags: Array<string>,
order_by: Array<string>,
languages?: Array<string>,
release_time?: string,
claim_type?: string | Array<string>,
timestamp?: string,
fee_amount?: string,
limit_claims_per_channel?: number,
stream_types?: Array<string>,
has_source?: boolean,
has_no_source?: boolean,
};
function resolveSearchOptions(props) {
const {
showNsfw,
@ -70,7 +98,8 @@ function resolveSearchOptions(props) {
streamTypesParam = [CS.FILE_VIDEO, CS.FILE_AUDIO];
}
const options = {
const options: SearchOptions = {
page: 1,
page_size: pageSize,
claim_type: claimType || ['stream', 'repost', 'channel'],
// no_totals makes it so the sdk doesn't have to calculate total number pages for pagination

View file

@ -1,32 +1,10 @@
// @flow
import type { Node } from 'react';
import React from 'react';
import { createNormalizedClaimSearchKey } from 'util/claim';
import ClaimPreviewTile from 'component/claimPreviewTile';
import useFetchViewCount from 'effects/use-fetch-view-count';
import usePrevious from 'effects/use-previous';
type SearchOptions = {
page_size: number,
page: number,
no_totals: boolean,
any_tags: Array<string>,
channel_ids: Array<string>,
claim_ids?: Array<string>,
not_channel_ids: Array<string>,
not_tags: Array<string>,
order_by: Array<string>,
languages?: Array<string>,
release_time?: string,
claim_type?: string | Array<string>,
timestamp?: string,
fee_amount?: string,
limit_claims_per_channel?: number,
stream_types?: Array<string>,
has_source?: boolean,
has_no_source?: boolean,
};
function urisEqual(prev: ?Array<string>, next: ?Array<string>) {
if (!prev || !next) {
// ClaimList: "null" and "undefined" have special meaning,
@ -68,12 +46,12 @@ type Props = {
hasNoSource?: boolean,
// --- select ---
location: { search: string },
claimSearchByQuery: { [string]: Array<string> },
claimSearchResults: Array<string>,
claimsByUri: { [string]: any },
fetchingClaimSearchByQuery: { [string]: boolean },
fetchingClaimSearch: boolean,
showNsfw: boolean,
hideReposts: boolean,
options: SearchOptions,
optionsStringified: string,
// --- perform ---
doClaimSearch: ({}) => void,
doFetchViewCount: (claimIdCsv: string) => void,
@ -82,10 +60,10 @@ type Props = {
function ClaimTilesDiscover(props: Props) {
const {
doClaimSearch,
claimSearchByQuery,
claimSearchResults,
claimsByUri,
fetchViewCount,
fetchingClaimSearchByQuery,
fetchingClaimSearch,
hasNoSource,
renderProperties,
pinUrls,
@ -93,16 +71,12 @@ function ClaimTilesDiscover(props: Props) {
showNoSourceClaims,
doFetchViewCount,
pageSize = 8,
options,
optionsStringified,
} = props;
const searchKey = createNormalizedClaimSearchKey(options);
const fetchingClaimSearch = fetchingClaimSearchByQuery[searchKey];
const claimSearchUris = claimSearchByQuery[searchKey] || [];
const isUnfetchedClaimSearch = claimSearchByQuery[searchKey] === undefined;
const claimSearchUris = claimSearchResults || [];
const isUnfetchedClaimSearch = claimSearchResults === undefined;
// Don't use the query from createNormalizedClaimSearchKey for the effect since that doesn't include page & release_time
const optionsStringForEffect = JSON.stringify(options);
const shouldPerformSearch = !fetchingClaimSearch && claimSearchUris.length === 0;
const uris = (prefixUris || []).concat(claimSearchUris);
@ -126,18 +100,21 @@ function ClaimTilesDiscover(props: Props) {
const prevUris = usePrevious(uris);
// Show previous results while we fetch to avoid blinkies and poor CLS.
const finalUris = isUnfetchedClaimSearch && prevUris ? prevUris : uris;
// --------------------------------------------------------------------------
// --------------------------------------------------------------------------
useFetchViewCount(fetchViewCount, uris, claimsByUri, doFetchViewCount);
// Run `doClaimSearch`
React.useEffect(() => {
if (shouldPerformSearch) {
const searchOptions = JSON.parse(optionsStringForEffect);
const searchOptions = JSON.parse(optionsStringified);
doClaimSearch(searchOptions);
}
}, [doClaimSearch, shouldPerformSearch, optionsStringForEffect]);
// Show previous results while we fetch to avoid blinkies and poor CLS.
const finalUris = isUnfetchedClaimSearch && prevUris ? prevUris : uris;
}, [doClaimSearch, shouldPerformSearch, optionsStringified]);
return (
<ul className="claim-grid">
@ -167,33 +144,33 @@ function ClaimTilesDiscover(props: Props) {
export default React.memo<Props>(ClaimTilesDiscover, areEqual);
function debug_trace(val) {
if (process.env.DEBUG_TRACE) console.log(`Render due to: ${val}`);
// ****************************************************************************
// ****************************************************************************
function trace(key, value) {
// @if process.env.DEBUG_TILE_RENDER
// $FlowFixMe "cannot coerce certain types".
console.log(`[claimTilesDiscover] ${key}: ${value}`); // eslint-disable-line no-console
// @endif
}
function areEqual(prev: Props, next: Props) {
const prevOptions: SearchOptions = prev.options;
const nextOptions: SearchOptions = next.options;
const prevSearchKey = createNormalizedClaimSearchKey(prevOptions);
const nextSearchKey = createNormalizedClaimSearchKey(nextOptions);
if (prevSearchKey !== nextSearchKey) {
debug_trace('search key');
return false;
}
// --- Deep-compare ---
if (!urisEqual(prev.claimSearchByQuery[prevSearchKey], next.claimSearchByQuery[nextSearchKey])) {
debug_trace('claimSearchByQuery');
return false;
// These are props that are hard to memoize from where it is passed.
if (prev.claimType !== next.claimType) {
// Array<string>: confirm the contents are actually different.
if (prev.claimType && next.claimType && JSON.stringify(prev.claimType) !== JSON.stringify(next.claimType)) {
trace('claimType', next.claimType);
return false;
}
}
const ARRAY_KEYS = ['prefixUris', 'channelIds'];
for (let i = 0; i < ARRAY_KEYS.length; ++i) {
const key = ARRAY_KEYS[i];
if (!urisEqual(prev[key], next[key])) {
debug_trace(`${key}`);
trace(key, next[key]);
return false;
}
}
@ -203,13 +180,11 @@ function areEqual(prev: Props, next: Props) {
// to update this function. Better to render more than miss an important one.
const KEYS_TO_IGNORE = [
...ARRAY_KEYS,
'claimSearchByQuery',
'fetchingClaimSearchByQuery', // We are showing previous results while fetching.
'options', // Covered by search-key comparison.
'claimType', // Handled above.
'claimsByUri', // Used for view-count. Just ignore it for now.
'location',
'history',
'match',
'claimsByUri',
'doClaimSearch',
];
@ -217,7 +192,7 @@ function areEqual(prev: Props, next: Props) {
for (let i = 0; i < propKeys.length; ++i) {
const pk = propKeys[i];
if (!KEYS_TO_IGNORE.includes(pk) && prev[pk] !== next[pk]) {
debug_trace(`${pk}`);
trace(pk, next[pk]);
return false;
}
}

View file

@ -616,10 +616,7 @@ export const makeSelectTagsForUri = (uri: string) =>
return (metadata && metadata.tags) || [];
});
export const selectFetchingClaimSearchByQuery = createSelector(
selectState,
(state) => state.fetchingClaimSearchByQuery || {}
);
export const selectFetchingClaimSearchByQuery = (state: State) => selectState(state).fetchingClaimSearchByQuery || {};
export const selectFetchingClaimSearch = createSelector(
selectFetchingClaimSearchByQuery,