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 { connect } from 'react-redux';
import { withRouter } from 'react-router'; import { withRouter } from 'react-router';
import { selectClaimSearchByQuery, selectFetchingClaimSearchByQuery, selectClaimsByUri } from 'redux/selectors/claims'; 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 { selectMutedAndBlockedChannelIds } from 'redux/selectors/blocked';
import { ENABLE_NO_SOURCE_CLAIMS, SIMPLE_SITE } from 'config'; import { ENABLE_NO_SOURCE_CLAIMS, SIMPLE_SITE } from 'config';
import * as CS from 'constants/claim_search'; import * as CS from 'constants/claim_search';
import { createNormalizedClaimSearchKey } from 'util/claim';
import ClaimListDiscover from './view'; import ClaimListDiscover from './view';
@ -17,13 +19,18 @@ const select = (state, props) => {
const hideReposts = selectClientSetting(state, SETTINGS.HIDE_REPOSTS); const hideReposts = selectClientSetting(state, SETTINGS.HIDE_REPOSTS);
const mutedAndBlockedChannelIds = selectMutedAndBlockedChannelIds(state); 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 { return {
claimSearchByQuery: selectClaimSearchByQuery(state), claimSearchResults: selectClaimSearchByQuery(state)[searchKey],
claimsByUri: selectClaimsByUri(state), claimsByUri: selectClaimsByUri(state),
fetchingClaimSearchByQuery: selectFetchingClaimSearchByQuery(state), fetchingClaimSearch: selectFetchingClaimSearchByQuery(state)[searchKey],
showNsfw, showNsfw,
hideReposts, 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) { function resolveSearchOptions(props) {
const { const {
showNsfw, showNsfw,
@ -70,7 +98,8 @@ function resolveSearchOptions(props) {
streamTypesParam = [CS.FILE_VIDEO, CS.FILE_AUDIO]; streamTypesParam = [CS.FILE_VIDEO, CS.FILE_AUDIO];
} }
const options = { const options: SearchOptions = {
page: 1,
page_size: pageSize, page_size: pageSize,
claim_type: claimType || ['stream', 'repost', 'channel'], claim_type: claimType || ['stream', 'repost', 'channel'],
// no_totals makes it so the sdk doesn't have to calculate total number pages for pagination // no_totals makes it so the sdk doesn't have to calculate total number pages for pagination

View file

@ -1,32 +1,10 @@
// @flow // @flow
import type { Node } from 'react'; import type { Node } from 'react';
import React from 'react'; import React from 'react';
import { createNormalizedClaimSearchKey } from 'util/claim';
import ClaimPreviewTile from 'component/claimPreviewTile'; import ClaimPreviewTile from 'component/claimPreviewTile';
import useFetchViewCount from 'effects/use-fetch-view-count'; import useFetchViewCount from 'effects/use-fetch-view-count';
import usePrevious from 'effects/use-previous'; 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>) { function urisEqual(prev: ?Array<string>, next: ?Array<string>) {
if (!prev || !next) { if (!prev || !next) {
// ClaimList: "null" and "undefined" have special meaning, // ClaimList: "null" and "undefined" have special meaning,
@ -68,12 +46,12 @@ type Props = {
hasNoSource?: boolean, hasNoSource?: boolean,
// --- select --- // --- select ---
location: { search: string }, location: { search: string },
claimSearchByQuery: { [string]: Array<string> }, claimSearchResults: Array<string>,
claimsByUri: { [string]: any }, claimsByUri: { [string]: any },
fetchingClaimSearchByQuery: { [string]: boolean }, fetchingClaimSearch: boolean,
showNsfw: boolean, showNsfw: boolean,
hideReposts: boolean, hideReposts: boolean,
options: SearchOptions, optionsStringified: string,
// --- perform --- // --- perform ---
doClaimSearch: ({}) => void, doClaimSearch: ({}) => void,
doFetchViewCount: (claimIdCsv: string) => void, doFetchViewCount: (claimIdCsv: string) => void,
@ -82,10 +60,10 @@ type Props = {
function ClaimTilesDiscover(props: Props) { function ClaimTilesDiscover(props: Props) {
const { const {
doClaimSearch, doClaimSearch,
claimSearchByQuery, claimSearchResults,
claimsByUri, claimsByUri,
fetchViewCount, fetchViewCount,
fetchingClaimSearchByQuery, fetchingClaimSearch,
hasNoSource, hasNoSource,
renderProperties, renderProperties,
pinUrls, pinUrls,
@ -93,16 +71,12 @@ function ClaimTilesDiscover(props: Props) {
showNoSourceClaims, showNoSourceClaims,
doFetchViewCount, doFetchViewCount,
pageSize = 8, pageSize = 8,
options, optionsStringified,
} = props; } = props;
const searchKey = createNormalizedClaimSearchKey(options); const claimSearchUris = claimSearchResults || [];
const fetchingClaimSearch = fetchingClaimSearchByQuery[searchKey]; const isUnfetchedClaimSearch = claimSearchResults === undefined;
const claimSearchUris = claimSearchByQuery[searchKey] || [];
const isUnfetchedClaimSearch = claimSearchByQuery[searchKey] === 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 shouldPerformSearch = !fetchingClaimSearch && claimSearchUris.length === 0;
const uris = (prefixUris || []).concat(claimSearchUris); const uris = (prefixUris || []).concat(claimSearchUris);
@ -126,18 +100,21 @@ function ClaimTilesDiscover(props: Props) {
const prevUris = usePrevious(uris); 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); useFetchViewCount(fetchViewCount, uris, claimsByUri, doFetchViewCount);
// Run `doClaimSearch` // Run `doClaimSearch`
React.useEffect(() => { React.useEffect(() => {
if (shouldPerformSearch) { if (shouldPerformSearch) {
const searchOptions = JSON.parse(optionsStringForEffect); const searchOptions = JSON.parse(optionsStringified);
doClaimSearch(searchOptions); doClaimSearch(searchOptions);
} }
}, [doClaimSearch, shouldPerformSearch, optionsStringForEffect]); }, [doClaimSearch, shouldPerformSearch, optionsStringified]);
// Show previous results while we fetch to avoid blinkies and poor CLS.
const finalUris = isUnfetchedClaimSearch && prevUris ? prevUris : uris;
return ( return (
<ul className="claim-grid"> <ul className="claim-grid">
@ -167,33 +144,33 @@ function ClaimTilesDiscover(props: Props) {
export default React.memo<Props>(ClaimTilesDiscover, areEqual); 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) { 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 --- // --- Deep-compare ---
if (!urisEqual(prev.claimSearchByQuery[prevSearchKey], next.claimSearchByQuery[nextSearchKey])) { // These are props that are hard to memoize from where it is passed.
debug_trace('claimSearchByQuery');
return false; 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']; const ARRAY_KEYS = ['prefixUris', 'channelIds'];
for (let i = 0; i < ARRAY_KEYS.length; ++i) { for (let i = 0; i < ARRAY_KEYS.length; ++i) {
const key = ARRAY_KEYS[i]; const key = ARRAY_KEYS[i];
if (!urisEqual(prev[key], next[key])) { if (!urisEqual(prev[key], next[key])) {
debug_trace(`${key}`); trace(key, next[key]);
return false; 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. // to update this function. Better to render more than miss an important one.
const KEYS_TO_IGNORE = [ const KEYS_TO_IGNORE = [
...ARRAY_KEYS, ...ARRAY_KEYS,
'claimSearchByQuery', 'claimType', // Handled above.
'fetchingClaimSearchByQuery', // We are showing previous results while fetching. 'claimsByUri', // Used for view-count. Just ignore it for now.
'options', // Covered by search-key comparison.
'location', 'location',
'history', 'history',
'match', 'match',
'claimsByUri',
'doClaimSearch', 'doClaimSearch',
]; ];
@ -217,7 +192,7 @@ function areEqual(prev: Props, next: Props) {
for (let i = 0; i < propKeys.length; ++i) { for (let i = 0; i < propKeys.length; ++i) {
const pk = propKeys[i]; const pk = propKeys[i];
if (!KEYS_TO_IGNORE.includes(pk) && prev[pk] !== next[pk]) { if (!KEYS_TO_IGNORE.includes(pk) && prev[pk] !== next[pk]) {
debug_trace(`${pk}`); trace(pk, next[pk]);
return false; return false;
} }
} }

View file

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