Optimise re-renders when scrolling / updating messages.

This commit is contained in:
Paul 2021-06-23 16:14:46 +01:00
parent 11c524d6a9
commit 0ce77951cb
4 changed files with 68 additions and 24 deletions

View file

@ -12,6 +12,7 @@ import MessageBase, { MessageContent, MessageDetail, MessageInfo } from "./Messa
import Overline from "../../ui/Overline"; import Overline from "../../ui/Overline";
import { useContext } from "preact/hooks"; import { useContext } from "preact/hooks";
import { AppContext } from "../../../context/revoltjs/RevoltClient"; import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { memo } from "preact/compat";
interface Props { interface Props {
attachContext?: boolean attachContext?: boolean
@ -22,7 +23,7 @@ interface Props {
head?: boolean 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: Can improve re-renders here by providing a list
// TODO: of dependencies. We only need to update on u/avatar. // TODO: of dependencies. We only need to update on u/avatar.
const user = useUser(message.author); const user = useUser(message.author);
@ -58,3 +59,5 @@ export default function Message({ attachContext, message, contrast, content: rep
</MessageBase> </MessageBase>
) )
} }
export default memo(Message);

View file

@ -1,3 +1 @@
export function defer(cb: () => void) { export const defer = (cb: () => void) => setTimeout(cb, 0);
setTimeout(cb, 0);
}

View file

@ -11,6 +11,7 @@ import { SingletonMessageRenderer } from "../../../lib/renderer/Singleton";
import { IntermediateContext } from "../../../context/intermediate/Intermediate"; import { IntermediateContext } from "../../../context/intermediate/Intermediate";
import { ClientStatus, StatusContext } from "../../../context/revoltjs/RevoltClient"; import { ClientStatus, StatusContext } from "../../../context/revoltjs/RevoltClient";
import { useContext, useEffect, useLayoutEffect, useRef, useState } from "preact/hooks"; import { useContext, useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
import { defer } from "../../../lib/defer";
const Area = styled.div` const Area = styled.div`
height: 100%; height: 100%;
@ -47,21 +48,61 @@ export function MessageArea({ id }: Props) {
// ? Current channel state. // ? Current channel state.
const [state, setState] = useState<RenderState>({ type: "LOADING" }); const [state, setState] = useState<RenderState>({ type: "LOADING" });
// ? Hook-based scrolling mechanism. // ? useRef to avoid re-renders
const [scrollState, setSS] = useState<ScrollState>({ const scrollState = useRef<ScrollState>({ type: "Free" });
type: "Free"
});
const setScrollState = (v: ScrollState) => { const setScrollState = (v: ScrollState) => {
if (v.type === 'StayAtBottom') { if (v.type === 'StayAtBottom') {
if (scrollState.type === 'Bottom' || atBottom()) { if (scrollState.current.type === 'Bottom' || atBottom()) {
setSS({ type: 'ScrollToBottom', smooth: v.smooth }); scrollState.current = { type: 'ScrollToBottom', smooth: v.smooth };
} else { } else {
setSS({ type: 'Free' }); scrollState.current = { type: 'Free' };
} }
} else { } 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. // ? 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. // ? Scroll to the bottom before the browser paints.
useLayoutEffect(() => { useLayoutEffect(() => {
if (scrollState.type === "ScrollToBottom") { // ! FIXME: NO REACTIVITY
if (scrollState.current.type === "ScrollToBottom") {
setScrollState({ type: "Bottom", scrollingUntil: + new Date() + 150 }); setScrollState({ type: "Bottom", scrollingUntil: + new Date() + 150 });
animateScroll.scrollToBottom({ animateScroll.scrollToBottom({
container: ref.current, 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( animateScroll.scrollTo(
Math.max( Math.max(
101, 101,
ref.current.scrollTop + ref.current.scrollTop +
(ref.current.scrollHeight - scrollState.previousHeight) (ref.current.scrollHeight - scrollState.current.previousHeight)
), ),
{ {
container: ref.current, container: ref.current,
@ -134,8 +176,8 @@ export function MessageArea({ id }: Props) {
); );
setScrollState({ type: "Free" }); setScrollState({ type: "Free" });
} else if (scrollState.type === "ScrollTop") { } else if (scrollState.current.type === "ScrollTop") {
animateScroll.scrollTo(scrollState.y, { animateScroll.scrollTo(scrollState.current.y, {
container: ref.current, container: ref.current,
duration: 0 duration: 0
}); });
@ -148,10 +190,10 @@ export function MessageArea({ id }: Props) {
// ? Also handle StayAtBottom // ? Also handle StayAtBottom
useEffect(() => { useEffect(() => {
async function onScroll() { async function onScroll() {
if (scrollState.type === "Free" && atBottom()) { if (scrollState.current.type === "Free" && atBottom()) {
setScrollState({ type: "Bottom" }); setScrollState({ type: "Bottom" });
} else if (scrollState.type === "Bottom" && !atBottom()) { } else if (scrollState.current.type === "Bottom" && !atBottom()) {
if (scrollState.scrollingUntil && scrollState.scrollingUntil > + new Date()) return; if (scrollState.current.scrollingUntil && scrollState.current.scrollingUntil > + new Date()) return;
setScrollState({ type: "Free" }); setScrollState({ type: "Free" });
} }
} }
@ -178,7 +220,7 @@ export function MessageArea({ id }: Props) {
// ? Scroll down whenever the message area resizes. // ? Scroll down whenever the message area resizes.
function stbOnResize() { function stbOnResize() {
if (!atBottom() && scrollState.type === "Bottom") { if (!atBottom() && scrollState.current.type === "Bottom") {
animateScroll.scrollToBottom({ animateScroll.scrollToBottom({
container: ref.current, container: ref.current,
duration: 0 duration: 0

View file

@ -1,4 +1,5 @@
import { decodeTime } from "ulid"; import { decodeTime } from "ulid";
import { memo } from "preact/compat";
import MessageEditor from "./MessageEditor"; import MessageEditor from "./MessageEditor";
import { Children } from "../../../types/Preact"; import { Children } from "../../../types/Preact";
import ConversationStart from "./ConversationStart"; import ConversationStart from "./ConversationStart";
@ -156,8 +157,8 @@ function MessageRenderer({ id, state, queue }: Props) {
return <>{ render }</>; return <>{ render }</>;
} }
export default connectState<Omit<Props, 'queue'>>(MessageRenderer, state => { export default memo(connectState<Omit<Props, 'queue'>>(MessageRenderer, state => {
return { return {
queue: state.queue queue: state.queue
}; };
}); }));