Optimize render part 2

This commit is contained in:
Fedor Indutny 2021-08-11 16:06:20 -07:00 committed by GitHub
parent 3f1adec614
commit d41e61a96b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 307 additions and 182 deletions

View file

@ -0,0 +1,55 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { ReactNode } from 'react';
type InternalPropsType = Readonly<{
id: string;
children: ReactNode;
onRender(
id: string,
phase: 'mount' | 'update',
actualDuration: number,
baseDuration: number,
startTime: number,
commitTime: number,
interactions: Set<unknown>
): void;
}>;
const Fallback: React.FC<InternalPropsType> = ({ children }) => {
return <>{children}</>;
};
const BaseProfiler: React.FC<InternalPropsType> =
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(React as any).unstable_Profiler || Fallback;
export type PropsType = Readonly<{
id: string;
children: ReactNode;
}>;
const onRender: InternalPropsType['onRender'] = (
id,
phase,
actual,
base,
start,
commit
) => {
window.log.info(
`Profiler.tsx(${id}): actual=${actual.toFixed(1)}ms phase=${phase} ` +
`base=${base.toFixed(1)}ms start=${start.toFixed(1)}ms ` +
`commit=${commit.toFixed(1)}ms`
);
};
export const Profiler: React.FC<PropsType> = ({ id, children }) => {
return (
<BaseProfiler id={id} onRender={onRender}>
{children}
</BaseProfiler>
);
};

View file

@ -11,7 +11,7 @@ import {
pick,
reduce,
} from 'lodash';
import { createSelector, createSelectorCreator } from 'reselect';
import { createSelectorCreator } from 'reselect';
import filesize from 'filesize';
import {
@ -102,108 +102,6 @@ export type GetPropsForBubbleOptions = Readonly<{
accountSelector: (identifier?: string) => boolean;
}>;
// Top-level prop generation for the message bubble
export function getPropsForBubble(
message: MessageAttributesType,
options: GetPropsForBubbleOptions
): TimelineItemType {
if (isUnsupportedMessage(message)) {
return {
type: 'unsupportedMessage',
data: getPropsForUnsupportedMessage(message, options),
};
}
if (isGroupV2Change(message)) {
return {
type: 'groupV2Change',
data: getPropsForGroupV2Change(message, options),
};
}
if (isGroupV1Migration(message)) {
return {
type: 'groupV1Migration',
data: getPropsForGroupV1Migration(message, options),
};
}
if (isMessageHistoryUnsynced(message)) {
return {
type: 'linkNotification',
data: null,
};
}
if (isExpirationTimerUpdate(message)) {
return {
type: 'timerNotification',
data: getPropsForTimerNotification(message, options),
};
}
if (isKeyChange(message)) {
return {
type: 'safetyNumberNotification',
data: getPropsForSafetyNumberNotification(message, options),
};
}
if (isVerifiedChange(message)) {
return {
type: 'verificationNotification',
data: getPropsForVerificationNotification(message, options),
};
}
if (isGroupUpdate(message)) {
return {
type: 'groupNotification',
data: getPropsForGroupNotification(message, options),
};
}
if (isEndSession(message)) {
return {
type: 'resetSessionNotification',
data: null,
};
}
if (isCallHistory(message)) {
return {
type: 'callHistory',
data: getPropsForCallHistory(message, options),
};
}
if (isProfileChange(message)) {
return {
type: 'profileChange',
data: getPropsForProfileChange(message, options),
};
}
if (isUniversalTimerNotification(message)) {
return {
type: 'universalTimerNotification',
data: null,
};
}
if (isChangeNumberNotification(message)) {
return {
type: 'changeNumberNotification',
data: getPropsForChangeNumberNotification(message, options),
};
}
if (isChatSessionRefreshed(message)) {
return {
type: 'chatSessionRefreshed',
data: null,
};
}
if (isDeliveryIssue(message)) {
return {
type: 'deliveryIssue',
data: getPropsForDeliveryIssue(message, options),
};
}
return {
type: 'message',
data: getPropsForMessage(message, options),
};
}
export function isIncoming(
message: Pick<MessageAttributesType, 'type'>
): boolean {
@ -483,6 +381,70 @@ export const getReactionsForMessage = createSelectorCreator(
(_: MessageAttributesType, reactions: PropsData['reactions']) => reactions
);
export const getPropsForQuote = createSelectorCreator(memoizeByRoot, isEqual)(
// `memoizeByRoot` requirement
identity,
(
message: Pick<MessageAttributesType, 'conversationId' | 'quote'>,
{
conversationSelector,
ourConversationId,
}: {
conversationSelector: GetConversationByIdType;
ourConversationId?: string;
}
): PropsData['quote'] => {
const { quote } = message;
if (!quote) {
return undefined;
}
const {
author,
authorUuid,
id: sentAt,
isViewOnce,
referencedMessageNotFound,
text,
} = quote;
const contact = conversationSelector(authorUuid || author);
const authorId = contact.id;
const authorName = contact.name;
const authorPhoneNumber = contact.phoneNumber;
const authorProfileName = contact.profileName;
const authorTitle = contact.title;
const isFromMe = authorId === ourConversationId;
const firstAttachment = quote.attachments && quote.attachments[0];
const conversation = getConversation(message, conversationSelector);
return {
authorId,
authorName,
authorPhoneNumber,
authorProfileName,
authorTitle,
bodyRanges: processBodyRanges(quote, { conversationSelector }),
conversationColor:
conversation.conversationColor ?? ConversationColors[0],
customColor: conversation.customColor,
isFromMe,
rawAttachment: firstAttachment
? processQuoteAttachment(firstAttachment)
: undefined,
isViewOnce,
referencedMessageNotFound,
sentAt: Number(sentAt),
text: createNonBreakingLastSeparator(text),
};
},
(_: unknown, quote: PropsData['quote']) => quote
);
export type GetPropsForMessageOptions = Pick<
GetPropsForBubbleOptions,
| 'conversationSelector'
@ -493,30 +455,51 @@ export type GetPropsForMessageOptions = Pick<
| 'accountSelector'
>;
export const getPropsForMessage = createSelector(
(message: MessageAttributesType) => message,
getAttachmentsForMessage,
processBodyRanges,
getAuthorForMessage,
getPreviewsForMessage,
getReactionsForMessage,
(_: unknown, options: GetPropsForMessageOptions) => options,
type ShallowPropsType = Pick<
PropsForMessage,
| 'canDeleteForEveryone'
| 'canDownload'
| 'canReply'
| 'contact'
| 'conversationColor'
| 'conversationId'
| 'conversationType'
| 'customColor'
| 'deletedForEveryone'
| 'direction'
| 'expirationLength'
| 'expirationTimestamp'
| 'id'
| 'isBlocked'
| 'isMessageRequestAccepted'
| 'isSelected'
| 'isSelectedCounter'
| 'isSticker'
| 'isTapToView'
| 'isTapToViewError'
| 'isTapToViewExpired'
| 'selectedReaction'
| 'status'
| 'text'
| 'textPending'
| 'timestamp'
>;
const getShallowPropsForMessage = createSelectorCreator(memoizeByRoot, isEqual)(
// `memoizeByRoot` requirement
identity,
(
message: MessageAttributesType,
attachments: Array<AttachmentType>,
bodyRanges: BodyRangesType | undefined,
author: PropsData['author'],
previews: Array<LinkPreviewType>,
reactions: PropsData['reactions'],
{
accountSelector,
conversationSelector,
ourConversationId,
regionCode,
selectedMessageId,
selectedMessageCounter,
regionCode,
accountSelector,
}: GetPropsForMessageOptions
): Omit<PropsForMessage, 'renderingContext'> => {
): ShallowPropsType => {
const { expireTimer, expirationStartTimestamp } = message;
const expirationLength = expireTimer ? expireTimer * 1000 : undefined;
const expirationTimestamp =
@ -530,17 +513,14 @@ export const getPropsForMessage = createSelector(
const isMessageTapToView = isTapToView(message);
const isSelected = message.id === selectedMessageId;
const selectedReaction = (
(message.reactions || []).find(re => re.fromId === ourConversationId) ||
{}
).emoji;
const isSelected = message.id === selectedMessageId;
return {
attachments,
author,
bodyRanges,
canDeleteForEveryone: canDeleteForEveryone(message),
canDownload: canDownload(message, conversationSelector),
canReply: canReply(message, ourConversationId, conversationSelector),
@ -564,18 +544,160 @@ export const getPropsForMessage = createSelector(
isTapToViewError:
isMessageTapToView && isIncoming(message) && message.isTapToViewInvalid,
isTapToViewExpired: isMessageTapToView && message.isErased,
previews,
quote: getPropsForQuote(message, conversationSelector, ourConversationId),
reactions,
selectedReaction,
status: getMessagePropStatus(message, ourConversationId),
text: createNonBreakingLastSeparator(message.body),
textPending: message.bodyPending,
timestamp: message.sent_at,
};
},
(_: unknown, props: ShallowPropsType) => props
);
export const getPropsForMessage = createSelectorCreator(memoizeByRoot)(
// `memoizeByRoot` requirement
identity,
getAttachmentsForMessage,
processBodyRanges,
getAuthorForMessage,
getPreviewsForMessage,
getReactionsForMessage,
getPropsForQuote,
getShallowPropsForMessage,
(
_: unknown,
attachments: Array<AttachmentType>,
bodyRanges: BodyRangesType | undefined,
author: PropsData['author'],
previews: Array<LinkPreviewType>,
reactions: PropsData['reactions'],
quote: PropsData['quote'],
shallowProps: ShallowPropsType
): Omit<PropsForMessage, 'renderingContext'> => {
return {
attachments,
author,
bodyRanges,
previews,
quote,
reactions,
...shallowProps,
};
}
);
export const getBubblePropsForMessage = createSelectorCreator(memoizeByRoot)(
// `memoizeByRoot` requirement
identity,
getPropsForMessage,
(_: unknown, data: ReturnType<typeof getPropsForMessage>) => ({
type: 'message' as const,
data,
})
);
// Top-level prop generation for the message bubble
export function getPropsForBubble(
message: MessageAttributesType,
options: GetPropsForBubbleOptions
): TimelineItemType {
if (isUnsupportedMessage(message)) {
return {
type: 'unsupportedMessage',
data: getPropsForUnsupportedMessage(message, options),
};
}
if (isGroupV2Change(message)) {
return {
type: 'groupV2Change',
data: getPropsForGroupV2Change(message, options),
};
}
if (isGroupV1Migration(message)) {
return {
type: 'groupV1Migration',
data: getPropsForGroupV1Migration(message, options),
};
}
if (isMessageHistoryUnsynced(message)) {
return {
type: 'linkNotification',
data: null,
};
}
if (isExpirationTimerUpdate(message)) {
return {
type: 'timerNotification',
data: getPropsForTimerNotification(message, options),
};
}
if (isKeyChange(message)) {
return {
type: 'safetyNumberNotification',
data: getPropsForSafetyNumberNotification(message, options),
};
}
if (isVerifiedChange(message)) {
return {
type: 'verificationNotification',
data: getPropsForVerificationNotification(message, options),
};
}
if (isGroupUpdate(message)) {
return {
type: 'groupNotification',
data: getPropsForGroupNotification(message, options),
};
}
if (isEndSession(message)) {
return {
type: 'resetSessionNotification',
data: null,
};
}
if (isCallHistory(message)) {
return {
type: 'callHistory',
data: getPropsForCallHistory(message, options),
};
}
if (isProfileChange(message)) {
return {
type: 'profileChange',
data: getPropsForProfileChange(message, options),
};
}
if (isUniversalTimerNotification(message)) {
return {
type: 'universalTimerNotification',
data: null,
};
}
if (isChangeNumberNotification(message)) {
return {
type: 'changeNumberNotification',
data: getPropsForChangeNumberNotification(message, options),
};
}
if (isChatSessionRefreshed(message)) {
return {
type: 'chatSessionRefreshed',
data: null,
};
}
if (isDeliveryIssue(message)) {
return {
type: 'deliveryIssue',
data: getPropsForDeliveryIssue(message, options),
};
}
return getBubblePropsForMessage(message, options);
}
// Unsupported Message
export function isUnsupportedMessage(message: MessageAttributesType): boolean {
@ -1168,57 +1290,6 @@ export function getPropsForAttachment(
};
}
export function getPropsForQuote(
message: Pick<MessageAttributesType, 'conversationId' | 'quote'>,
conversationSelector: GetConversationByIdType,
ourConversationId: string | undefined
): PropsData['quote'] {
const { quote } = message;
if (!quote) {
return undefined;
}
const {
author,
authorUuid,
id: sentAt,
isViewOnce,
referencedMessageNotFound,
text,
} = quote;
const contact = conversationSelector(authorUuid || author);
const authorId = contact.id;
const authorName = contact.name;
const authorPhoneNumber = contact.phoneNumber;
const authorProfileName = contact.profileName;
const authorTitle = contact.title;
const isFromMe = authorId === ourConversationId;
const firstAttachment = quote.attachments && quote.attachments[0];
const conversation = getConversation(message, conversationSelector);
return {
authorId,
authorName,
authorPhoneNumber,
authorProfileName,
authorTitle,
bodyRanges: processBodyRanges(quote, { conversationSelector }),
conversationColor: conversation.conversationColor ?? ConversationColors[0],
customColor: conversation.customColor,
isFromMe,
rawAttachment: firstAttachment
? processQuoteAttachment(firstAttachment)
: undefined,
isViewOnce,
referencedMessageNotFound,
sentAt: Number(sentAt),
text: createNonBreakingLastSeparator(text),
};
}
function processQuoteAttachment(
attachment: AttachmentType
): QuotedAttachmentType {

View file

@ -87,11 +87,10 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
linkPreviewResult,
// Quote
quotedMessageProps: quotedMessage
? getPropsForQuote(
quotedMessage,
? getPropsForQuote(quotedMessage, {
conversationSelector,
getUserConversationId(state)
)
ourConversationId: getUserConversationId(state),
})
: undefined,
onClickQuotedMessage: () =>
onClickQuotedMessage(quotedMessage?.quote?.messageId),