From 7a8363c7c84578e0630039a63bad37f9de6b4e18 Mon Sep 17 00:00:00 2001 From: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com> Date: Wed, 18 Aug 2021 06:34:22 -0700 Subject: [PATCH] Additional render optimizations --- ts/components/conversation/Timeline.tsx | 137 +++++++++++++----------- ts/state/ducks/composer.ts | 13 +++ ts/state/ducks/linkPreviews.ts | 9 +- ts/views/conversation_view.ts | 4 +- 4 files changed, 93 insertions(+), 70 deletions(-) diff --git a/ts/components/conversation/Timeline.tsx b/ts/components/conversation/Timeline.tsx index 77b9844296..ae6eb27af6 100644 --- a/ts/components/conversation/Timeline.tsx +++ b/ts/components/conversation/Timeline.tsx @@ -1,9 +1,10 @@ // Copyright 2019-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import { debounce, get, isNumber, pick } from 'lodash'; +import { debounce, get, isNumber, pick, identity } from 'lodash'; import classNames from 'classnames'; import React, { CSSProperties, ReactChild, ReactNode } from 'react'; +import { createSelector } from 'reselect'; import { AutoSizer, CellMeasurer, @@ -214,6 +215,74 @@ type StateType = { lastMeasuredWarningHeight: number; }; +const getActions = createSelector( + // It is expensive to pick so many properties out of the `props` object so we + // use `createSelector` to memoize them by the last seen `props` object. + identity, + + (props: PropsType): PropsActionsType => { + const unsafe = pick(props, [ + 'acknowledgeGroupMemberNameCollisions', + 'clearChangedMessages', + 'clearInvitedConversationsForNewlyCreatedGroup', + 'closeContactSpoofingReview', + 'setLoadCountdownStart', + 'setIsNearBottom', + 'reviewGroupMemberNameCollision', + 'reviewMessageRequestNameCollision', + 'learnMoreAboutDeliveryIssue', + 'loadAndScroll', + 'loadOlderMessages', + 'loadNewerMessages', + 'loadNewestMessages', + 'markMessageRead', + 'markViewed', + 'onBlock', + 'onBlockAndReportSpam', + 'onDelete', + 'onUnblock', + 'removeMember', + 'selectMessage', + 'clearSelectedMessage', + 'unblurAvatar', + 'updateSharedGroups', + + 'doubleCheckMissingQuoteReference', + 'onHeightChange', + 'checkForAccount', + 'reactToMessage', + 'replyToMessage', + 'retrySend', + 'showForwardMessageModal', + 'deleteMessage', + 'deleteMessageForEveryone', + 'showMessageDetail', + 'openConversation', + 'showContactDetail', + 'showContactModal', + 'kickOffAttachmentDownload', + 'markAttachmentAsCorrupted', + 'showVisualAttachment', + 'downloadAttachment', + 'displayTapToViewMessage', + 'openLink', + 'scrollToQuotedMessage', + 'showExpiredIncomingTapToViewToast', + 'showExpiredOutgoingTapToViewToast', + + 'showIdentity', + + 'downloadNewVersion', + + 'contactSupport', + ]); + + const safe: AssertProps = unsafe; + + return safe; + } +); + export class Timeline extends React.PureComponent { public cellSizeCache = new CellMeasurerCache({ defaultHeight: 64, @@ -728,6 +797,8 @@ export class Timeline extends React.PureComponent { const messageId = items[itemIndex]; stableKey = messageId; + const actions = getActions(this.props); + rowContents = (
{ role="row" > window.showDebugLog()}> - {renderItem(messageId, id, this.resizeMessage, this.getActions())} + {renderItem(messageId, id, this.resizeMessage, actions)}
); @@ -1481,66 +1552,4 @@ export class Timeline extends React.PureComponent { throw missingCaseError(warning); } } - - private getActions(): PropsActionsType { - const unsafe = pick(this.props, [ - 'acknowledgeGroupMemberNameCollisions', - 'clearChangedMessages', - 'clearInvitedConversationsForNewlyCreatedGroup', - 'closeContactSpoofingReview', - 'setLoadCountdownStart', - 'setIsNearBottom', - 'reviewGroupMemberNameCollision', - 'reviewMessageRequestNameCollision', - 'learnMoreAboutDeliveryIssue', - 'loadAndScroll', - 'loadOlderMessages', - 'loadNewerMessages', - 'loadNewestMessages', - 'markMessageRead', - 'markViewed', - 'onBlock', - 'onBlockAndReportSpam', - 'onDelete', - 'onUnblock', - 'removeMember', - 'selectMessage', - 'clearSelectedMessage', - 'unblurAvatar', - 'updateSharedGroups', - - 'doubleCheckMissingQuoteReference', - 'onHeightChange', - 'checkForAccount', - 'reactToMessage', - 'replyToMessage', - 'retrySend', - 'showForwardMessageModal', - 'deleteMessage', - 'deleteMessageForEveryone', - 'showMessageDetail', - 'openConversation', - 'showContactDetail', - 'showContactModal', - 'kickOffAttachmentDownload', - 'markAttachmentAsCorrupted', - 'showVisualAttachment', - 'downloadAttachment', - 'displayTapToViewMessage', - 'openLink', - 'scrollToQuotedMessage', - 'showExpiredIncomingTapToViewToast', - 'showExpiredOutgoingTapToViewToast', - - 'showIdentity', - - 'downloadNewVersion', - - 'contactSupport', - ]); - - const safe: AssertProps = unsafe; - - return safe; - } } diff --git a/ts/state/ducks/composer.ts b/ts/state/ducks/composer.ts index 4d95d14f5d..5983716fba 100644 --- a/ts/state/ducks/composer.ts +++ b/ts/state/ducks/composer.ts @@ -7,6 +7,11 @@ import { StateType as RootStateType } from '../reducer'; import { AttachmentType } from '../../types/Attachment'; import { MessageAttributesType } from '../../model-types.d'; import { LinkPreviewWithDomain } from '../../types/LinkPreview'; +import { assignWithNoUnnecessaryAllocation } from '../../util/assignWithNoUnnecessaryAllocation'; +import { + REMOVE_PREVIEW as REMOVE_LINK_PREVIEW, + RemoveLinkPreviewActionType, +} from './linkPreviews'; // State @@ -58,6 +63,7 @@ type ComposerActionType = | ResetComposerActionType | SetHighQualitySettingActionType | SetLinkPreviewResultActionType + | RemoveLinkPreviewActionType | SetQuotedMessageActionType; // Action Creators @@ -176,5 +182,12 @@ export function reducer( }; } + if (action.type === REMOVE_LINK_PREVIEW) { + return assignWithNoUnnecessaryAllocation(state, { + linkPreviewLoading: false, + linkPreviewResult: undefined, + }); + } + return state; } diff --git a/ts/state/ducks/linkPreviews.ts b/ts/state/ducks/linkPreviews.ts index bc820fa9c2..fa390c925a 100644 --- a/ts/state/ducks/linkPreviews.ts +++ b/ts/state/ducks/linkPreviews.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import { LinkPreviewType } from '../../types/message/LinkPreviews'; +import { assignWithNoUnnecessaryAllocation } from '../../util/assignWithNoUnnecessaryAllocation'; // State @@ -12,14 +13,14 @@ export type LinkPreviewsStateType = { // Actions const ADD_PREVIEW = 'linkPreviews/ADD_PREVIEW'; -const REMOVE_PREVIEW = 'linkPreviews/REMOVE_PREVIEW'; +export const REMOVE_PREVIEW = 'linkPreviews/REMOVE_PREVIEW'; type AddLinkPreviewActionType = { type: 'linkPreviews/ADD_PREVIEW'; payload: LinkPreviewType; }; -type RemoveLinkPreviewActionType = { +export type RemoveLinkPreviewActionType = { type: 'linkPreviews/REMOVE_PREVIEW'; }; @@ -68,9 +69,9 @@ export function reducer( } if (action.type === REMOVE_PREVIEW) { - return { + return assignWithNoUnnecessaryAllocation(state, { linkPreview: undefined, - }; + }); } return state; diff --git a/ts/views/conversation_view.ts b/ts/views/conversation_view.ts index bf556d5148..292fcb20e3 100644 --- a/ts/views/conversation_view.ts +++ b/ts/views/conversation_view.ts @@ -4137,12 +4137,12 @@ Whisper.ConversationView = Whisper.View.extend({ URL.revokeObjectURL(item.url); } }); - window.reduxActions.linkPreviews.removeLinkPreview(); this.preview = null; this.currentlyMatchedLink = null; this.linkPreviewAbortController?.abort(); this.linkPreviewAbortController = null; - this.renderLinkPreview(); + + window.reduxActions.linkPreviews.removeLinkPreview(); }, async getStickerPackPreview(