From 0ce77951cb531bc3fd6dc74422650f7e234e8a08 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 23 Jun 2021 16:14:46 +0100 Subject: [PATCH] Optimise re-renders when scrolling / updating messages. --- src/components/common/messaging/Message.tsx | 5 +- src/lib/defer.ts | 4 +- src/pages/channels/messaging/MessageArea.tsx | 78 ++++++++++++++----- .../channels/messaging/MessageRenderer.tsx | 5 +- 4 files changed, 68 insertions(+), 24 deletions(-) diff --git a/src/components/common/messaging/Message.tsx b/src/components/common/messaging/Message.tsx index 28ee957c..92d6c362 100644 --- a/src/components/common/messaging/Message.tsx +++ b/src/components/common/messaging/Message.tsx @@ -12,6 +12,7 @@ import MessageBase, { MessageContent, MessageDetail, MessageInfo } from "./Messa import Overline from "../../ui/Overline"; import { useContext } from "preact/hooks"; import { AppContext } from "../../../context/revoltjs/RevoltClient"; +import { memo } from "preact/compat"; interface Props { attachContext?: boolean @@ -22,7 +23,7 @@ interface Props { head?: boolean } -export default function Message({ attachContext, message, contrast, content: replacement, head: preferHead, queued }: Props) { +function Message({ attachContext, message, contrast, content: replacement, head: preferHead, queued }: Props) { // TODO: Can improve re-renders here by providing a list // TODO: of dependencies. We only need to update on u/avatar. const user = useUser(message.author); @@ -58,3 +59,5 @@ export default function Message({ attachContext, message, contrast, content: rep ) } + +export default memo(Message); diff --git a/src/lib/defer.ts b/src/lib/defer.ts index 2ad2d46c..79de63ae 100644 --- a/src/lib/defer.ts +++ b/src/lib/defer.ts @@ -1,3 +1 @@ -export function defer(cb: () => void) { - setTimeout(cb, 0); -} +export const defer = (cb: () => void) => setTimeout(cb, 0); diff --git a/src/pages/channels/messaging/MessageArea.tsx b/src/pages/channels/messaging/MessageArea.tsx index a178fab8..11cc849b 100644 --- a/src/pages/channels/messaging/MessageArea.tsx +++ b/src/pages/channels/messaging/MessageArea.tsx @@ -11,6 +11,7 @@ import { SingletonMessageRenderer } from "../../../lib/renderer/Singleton"; import { IntermediateContext } from "../../../context/intermediate/Intermediate"; import { ClientStatus, StatusContext } from "../../../context/revoltjs/RevoltClient"; import { useContext, useEffect, useLayoutEffect, useRef, useState } from "preact/hooks"; +import { defer } from "../../../lib/defer"; const Area = styled.div` height: 100%; @@ -47,21 +48,61 @@ export function MessageArea({ id }: Props) { // ? Current channel state. const [state, setState] = useState({ type: "LOADING" }); - // ? Hook-based scrolling mechanism. - const [scrollState, setSS] = useState({ - type: "Free" - }); + // ? useRef to avoid re-renders + const scrollState = useRef({ type: "Free" }); const setScrollState = (v: ScrollState) => { if (v.type === 'StayAtBottom') { - if (scrollState.type === 'Bottom' || atBottom()) { - setSS({ type: 'ScrollToBottom', smooth: v.smooth }); + if (scrollState.current.type === 'Bottom' || atBottom()) { + scrollState.current = { type: 'ScrollToBottom', smooth: v.smooth }; } else { - setSS({ type: 'Free' }); + scrollState.current = { type: 'Free' }; } } else { - setSS(v); + scrollState.current = v; } + + defer(() => { + if (scrollState.current.type === "ScrollToBottom") { + setScrollState({ type: "Bottom", scrollingUntil: + new Date() + 150 }); + + animateScroll.scrollToBottom({ + container: ref.current, + duration: scrollState.current.smooth ? 150 : 0 + }); + } else if (scrollState.current.type === "OffsetTop") { + animateScroll.scrollTo( + Math.max( + 101, + ref.current.scrollTop + + (ref.current.scrollHeight - scrollState.current.previousHeight) + ), + { + container: ref.current, + duration: 0 + } + ); + + setScrollState({ type: "Free" }); + } else if (scrollState.current.type === "ScrollTop") { + animateScroll.scrollTo(scrollState.current.y, { + container: ref.current, + duration: 0 + }); + + setScrollState({ type: "Free" }); + } + }); + + /*if (v.type === 'StayAtBottom') { + if (scrollState.current.type === 'Bottom' || atBottom()) { + scrollState.current = { type: 'ScrollToBottom', smooth: v.smooth }; + } else { + scrollState.current = { type: 'Free' }; + } + } else { + scrollState.current = v; + }*/ } // ? Determine if we are at the bottom of the scroll container. @@ -113,19 +154,20 @@ export function MessageArea({ id }: Props) { // ? Scroll to the bottom before the browser paints. useLayoutEffect(() => { - if (scrollState.type === "ScrollToBottom") { + // ! FIXME: NO REACTIVITY + if (scrollState.current.type === "ScrollToBottom") { setScrollState({ type: "Bottom", scrollingUntil: + new Date() + 150 }); animateScroll.scrollToBottom({ container: ref.current, - duration: scrollState.smooth ? 150 : 0 + duration: scrollState.current.smooth ? 150 : 0 }); - } else if (scrollState.type === "OffsetTop") { + } else if (scrollState.current.type === "OffsetTop") { animateScroll.scrollTo( Math.max( 101, ref.current.scrollTop + - (ref.current.scrollHeight - scrollState.previousHeight) + (ref.current.scrollHeight - scrollState.current.previousHeight) ), { container: ref.current, @@ -134,8 +176,8 @@ export function MessageArea({ id }: Props) { ); setScrollState({ type: "Free" }); - } else if (scrollState.type === "ScrollTop") { - animateScroll.scrollTo(scrollState.y, { + } else if (scrollState.current.type === "ScrollTop") { + animateScroll.scrollTo(scrollState.current.y, { container: ref.current, duration: 0 }); @@ -148,10 +190,10 @@ export function MessageArea({ id }: Props) { // ? Also handle StayAtBottom useEffect(() => { async function onScroll() { - if (scrollState.type === "Free" && atBottom()) { + if (scrollState.current.type === "Free" && atBottom()) { setScrollState({ type: "Bottom" }); - } else if (scrollState.type === "Bottom" && !atBottom()) { - if (scrollState.scrollingUntil && scrollState.scrollingUntil > + new Date()) return; + } else if (scrollState.current.type === "Bottom" && !atBottom()) { + if (scrollState.current.scrollingUntil && scrollState.current.scrollingUntil > + new Date()) return; setScrollState({ type: "Free" }); } } @@ -178,7 +220,7 @@ export function MessageArea({ id }: Props) { // ? Scroll down whenever the message area resizes. function stbOnResize() { - if (!atBottom() && scrollState.type === "Bottom") { + if (!atBottom() && scrollState.current.type === "Bottom") { animateScroll.scrollToBottom({ container: ref.current, duration: 0 diff --git a/src/pages/channels/messaging/MessageRenderer.tsx b/src/pages/channels/messaging/MessageRenderer.tsx index 2c665827..fba6b281 100644 --- a/src/pages/channels/messaging/MessageRenderer.tsx +++ b/src/pages/channels/messaging/MessageRenderer.tsx @@ -1,4 +1,5 @@ import { decodeTime } from "ulid"; +import { memo } from "preact/compat"; import MessageEditor from "./MessageEditor"; import { Children } from "../../../types/Preact"; import ConversationStart from "./ConversationStart"; @@ -156,8 +157,8 @@ function MessageRenderer({ id, state, queue }: Props) { return <>{ render }; } -export default connectState>(MessageRenderer, state => { +export default memo(connectState>(MessageRenderer, state => { return { queue: state.queue }; -}); +}));