[Perf] Teact: Optimize updating attributes and support dangerouslySetInnerHTML

This commit is contained in:
Alexander Zinchuk 2021-11-27 17:40:44 +01:00
parent c2d76a9990
commit 528137daff
2 changed files with 30 additions and 40 deletions

View File

@ -3,7 +3,6 @@ import React, {
memo, memo,
useCallback, useCallback,
useEffect, useEffect,
useLayoutEffect,
useMemo, useMemo,
useRef, useRef,
} from '../../../lib/teact/teact'; } from '../../../lib/teact/teact';
@ -168,9 +167,9 @@ type DispatchProps = Pick<GlobalActions, 'toggleMessageSelection' | 'clickInline
const NBSP = '\u00A0'; const NBSP = '\u00A0';
const GROUP_MESSAGE_HOVER_ATTRIBUTE = 'data-is-document-group-hover'; const GROUP_MESSAGE_HOVER_ATTRIBUTE = 'data-is-document-group-hover';
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
const APPENDIX_OWN = '<svg width="9" height="20" xmlns="http://www.w3.org/2000/svg"><defs><filter x="-50%" y="-14.7%" width="200%" height="141.2%" filterUnits="objectBoundingBox" id="a"><feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"/><feColorMatrix values="0 0 0 0 0.0621962482 0 0 0 0 0.138574144 0 0 0 0 0.185037364 0 0 0 0.15 0" in="shadowBlurOuter1"/></filter></defs><g fill="none" fill-rule="evenodd"><path d="M6 17H0V0c.193 2.84.876 5.767 2.05 8.782.904 2.325 2.446 4.485 4.625 6.48A1 1 0 016 17z" fill="#000" filter="url(#a)"/><path d="M6 17H0V0c.193 2.84.876 5.767 2.05 8.782.904 2.325 2.446 4.485 4.625 6.48A1 1 0 016 17z" fill="#EEFFDE" class="corner"/></g></svg>'; const APPENDIX_OWN = { __html: '<svg width="9" height="20" xmlns="http://www.w3.org/2000/svg"><defs><filter x="-50%" y="-14.7%" width="200%" height="141.2%" filterUnits="objectBoundingBox" id="a"><feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"/><feColorMatrix values="0 0 0 0 0.0621962482 0 0 0 0 0.138574144 0 0 0 0 0.185037364 0 0 0 0.15 0" in="shadowBlurOuter1"/></filter></defs><g fill="none" fill-rule="evenodd"><path d="M6 17H0V0c.193 2.84.876 5.767 2.05 8.782.904 2.325 2.446 4.485 4.625 6.48A1 1 0 016 17z" fill="#000" filter="url(#a)"/><path d="M6 17H0V0c.193 2.84.876 5.767 2.05 8.782.904 2.325 2.446 4.485 4.625 6.48A1 1 0 016 17z" fill="#EEFFDE" class="corner"/></g></svg>' };
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
const APPENDIX_NOT_OWN = '<svg width="9" height="20" xmlns="http://www.w3.org/2000/svg"><defs><filter x="-50%" y="-14.7%" width="200%" height="141.2%" filterUnits="objectBoundingBox" id="a"><feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"/><feColorMatrix values="0 0 0 0 0.0621962482 0 0 0 0 0.138574144 0 0 0 0 0.185037364 0 0 0 0.15 0" in="shadowBlurOuter1"/></filter></defs><g fill="none" fill-rule="evenodd"><path d="M3 17h6V0c-.193 2.84-.876 5.767-2.05 8.782-.904 2.325-2.446 4.485-4.625 6.48A1 1 0 003 17z" fill="#000" filter="url(#a)"/><path d="M3 17h6V0c-.193 2.84-.876 5.767-2.05 8.782-.904 2.325-2.446 4.485-4.625 6.48A1 1 0 003 17z" fill="#FFF" class="corner"/></g></svg>'; const APPENDIX_NOT_OWN = { __html: '<svg width="9" height="20" xmlns="http://www.w3.org/2000/svg"><defs><filter x="-50%" y="-14.7%" width="200%" height="141.2%" filterUnits="objectBoundingBox" id="a"><feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"/><feColorMatrix values="0 0 0 0 0.0621962482 0 0 0 0 0.138574144 0 0 0 0 0.185037364 0 0 0 0.15 0" in="shadowBlurOuter1"/></filter></defs><g fill="none" fill-rule="evenodd"><path d="M3 17h6V0c-.193 2.84-.876 5.767-2.05 8.782-.904 2.325-2.446 4.485-4.625 6.48A1 1 0 003 17z" fill="#000" filter="url(#a)"/><path d="M3 17h6V0c-.193 2.84-.876 5.767-2.05 8.782-.904 2.325-2.446 4.485-4.625 6.48A1 1 0 003 17z" fill="#FFF" class="corner"/></g></svg>' };
const APPEARANCE_DELAY = 10; const APPEARANCE_DELAY = 10;
const NO_MEDIA_CORNERS_THRESHOLD = 18; const NO_MEDIA_CORNERS_THRESHOLD = 18;
@ -233,8 +232,7 @@ const Message: FC<OwnProps & StateProps & DispatchProps> = ({
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
const bottomMarkerRef = useRef<HTMLDivElement>(null); const bottomMarkerRef = useRef<HTMLDivElement>(null);
// eslint-disable-next-line no-null/no-null
const appendixRef = useRef<HTMLDivElement>(null);
const lang = useLang(); const lang = useLang();
useOnIntersect(bottomMarkerRef, observeIntersectionForBottom); useOnIntersect(bottomMarkerRef, observeIntersectionForBottom);
@ -405,13 +403,6 @@ const Message: FC<OwnProps & StateProps & DispatchProps> = ({
message.id, message.id,
); );
useFocusMessage(ref, chatId, isFocused, focusDirection, noFocusHighlight, isResizingContainer); useFocusMessage(ref, chatId, isFocused, focusDirection, noFocusHighlight, isResizingContainer);
useLayoutEffect(() => {
if (!appendixRef.current) {
return;
}
appendixRef.current.innerHTML = isOwn ? APPENDIX_OWN : APPENDIX_NOT_OWN;
}, [isOwn, withAppendix]);
let style = ''; let style = '';
let calculatedWidth; let calculatedWidth;
@ -766,7 +757,9 @@ const Message: FC<OwnProps & StateProps & DispatchProps> = ({
</Button> </Button>
) : undefined} ) : undefined}
{withCommentButton && <CommentButton message={message} disabled={noComments} />} {withCommentButton && <CommentButton message={message} disabled={noComments} />}
{withAppendix && <div className="svg-appendix" ref={appendixRef} />} {withAppendix && (
<div className="svg-appendix" dangerouslySetInnerHTML={isOwn ? APPENDIX_OWN : APPENDIX_NOT_OWN} />
)}
</div> </div>
{message.inlineButtons && ( {message.inlineButtons && (
<InlineButtons message={message} onClick={clickInlineButton} /> <InlineButtons message={message} onClick={clickInlineButton} />

View File

@ -223,7 +223,9 @@ function createNode($element: VirtualElement): Node {
} }
Object.keys(props).forEach((key) => { Object.keys(props).forEach((key) => {
addAttribute(element, key, props[key]); if (props[key] !== undefined) {
setAttribute(element, key, props[key]);
}
}); });
$element.children = children.map(($child, i) => ( $element.children = children.map(($child, i) => (
@ -395,31 +397,31 @@ function updateAttributes($current: VirtualRealElement, $new: VirtualRealElement
const newKeys = Object.keys($new.props); const newKeys = Object.keys($new.props);
currentKeys.forEach((key) => { currentKeys.forEach((key) => {
if ($current.props[key] !== undefined && $new.props[key] === undefined) { const currentValue = $current.props[key];
removeAttribute(element, key, $current.props[key]); const newValue = $new.props[key];
if (
currentValue !== undefined
&& (
newValue === undefined
|| (currentValue !== newValue && key.startsWith('on'))
)
) {
removeAttribute(element, key, currentValue);
} }
}); });
newKeys.forEach((key) => { newKeys.forEach((key) => {
if ($new.props[key] === undefined) { const currentValue = $current.props[key];
return; const newValue = $new.props[key];
}
if ($current.props[key] !== $new.props[key]) { if (newValue !== undefined && newValue !== currentValue) {
if ($current.props[key] === undefined) { setAttribute(element, key, newValue);
addAttribute(element, key, $new.props[key]);
} else {
updateAttribute(element, key, $current.props[key], $new.props[key]);
}
} }
}); });
} }
function addAttribute(element: HTMLElement, key: string, value: any) { function setAttribute(element: HTMLElement, key: string, value: any) {
if (value === undefined) {
return;
}
// An optimization attempt // An optimization attempt
if (key === 'className') { if (key === 'className') {
element.className = value; element.className = value;
@ -428,6 +430,9 @@ function addAttribute(element: HTMLElement, key: string, value: any) {
(element as HTMLInputElement).value = value; (element as HTMLInputElement).value = value;
} else if (key === 'style') { } else if (key === 'style') {
element.style.cssText = value; element.style.cssText = value;
} else if (key === 'dangerouslySetInnerHTML') {
// eslint-disable-next-line no-underscore-dangle
element.innerHTML = value.__html;
} else if (key.startsWith('on')) { } else if (key.startsWith('on')) {
addEventListener(element, key, value, key.endsWith('Capture')); addEventListener(element, key, value, key.endsWith('Capture'));
} else if (key.startsWith('data-') || HTML_ATTRIBUTES.has(key)) { } else if (key.startsWith('data-') || HTML_ATTRIBUTES.has(key)) {
@ -444,6 +449,8 @@ function removeAttribute(element: HTMLElement, key: string, value: any) {
(element as HTMLInputElement).value = ''; (element as HTMLInputElement).value = '';
} else if (key === 'style') { } else if (key === 'style') {
element.style.cssText = ''; element.style.cssText = '';
} else if (key === 'dangerouslySetInnerHTML') {
element.innerHTML = '';
} else if (key.startsWith('on')) { } else if (key.startsWith('on')) {
removeEventListener(element, key, value, key.endsWith('Capture')); removeEventListener(element, key, value, key.endsWith('Capture'));
} else if (key.startsWith('data-') || HTML_ATTRIBUTES.has(key)) { } else if (key.startsWith('data-') || HTML_ATTRIBUTES.has(key)) {
@ -453,16 +460,6 @@ function removeAttribute(element: HTMLElement, key: string, value: any) {
} }
} }
function updateAttribute(element: HTMLElement, key: string, oldValue: any, newValue: any) {
if (key === 'value') {
// Removing and adding value causes a cursor jump
(element as HTMLInputElement).value = newValue !== undefined ? newValue : '';
} else {
removeAttribute(element, key, oldValue);
addAttribute(element, key, newValue);
}
}
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
function DEBUG_addToVirtualTreeSize($current: VirtualRealElement | VirtualDomHead) { function DEBUG_addToVirtualTreeSize($current: VirtualRealElement | VirtualDomHead) {
DEBUG_virtualTreeSize += $current.children.length; DEBUG_virtualTreeSize += $current.children.length;