refactor: 优化消息加载 (#288)

* refactor: 尝试优化消息加载

- Updated webpack configuration to exclude additional sourcemaps.
- Changed local development URL for easier access.
- Implemented batch processing for handling chat messages to prevent main thread blocking.
- Refined message component to selectively subscribe to user and reaction data.
- Enhanced virtual message feed to manage visible message count and improve loading performance.

* chore: bump version to v0.9.44
This commit is contained in:
haorwen
2025-12-31 00:15:36 +08:00
committed by GitHub
parent dea7e0446e
commit 7f14b4b65e
6 changed files with 94 additions and 47 deletions
+18 -3
View File
@@ -1,4 +1,5 @@
import { createApi } from "@reduxjs/toolkit/query/react";
import { batch } from "react-redux";
import { ChatContext } from "@/types/common";
import { ChatMessage, ContentTypeKey, UploadFileResponse } from "@/types/message";
@@ -224,9 +225,23 @@ export const messageApi = createApi({
const { data: messages } = await queryFulfilled;
const fromHistory = true;
if (messages?.length) {
messages.forEach((msg) => {
handleChatMessage(msg, dispatch, getState() as RootState, fromHistory);
});
// 分批处理消息,避免阻塞主线程
const batchSize = 20;
const processBatch = (startIndex: number) => {
const endIndex = Math.min(startIndex + batchSize, messages.length);
batch(() => {
for (let i = startIndex; i < endIndex; i++) {
handleChatMessage(messages[i], dispatch, getState() as RootState, fromHistory);
}
});
if (endIndex < messages.length) {
// 使用 setTimeout 让出主线程,继续处理下一批
setTimeout(() => processBatch(endIndex), 0);
}
};
processBatch(0);
}
}
}),
+13 -9
View File
@@ -49,8 +49,10 @@ const Message: FC<IProps> = ({
shallowEqual
);
const loginUid = useAppSelector((store) => store.authData.user?.uid, shallowEqual);
const usersData = useAppSelector((store) => store.users.byId, shallowEqual);
const reactionMessageData = useAppSelector((store) => store.reactionMessage, shallowEqual);
// 只订阅当前消息发送者的用户信息,而不是整个usersData
const currUser = useAppSelector((store) => store.users.byId[message?.from_uid || 0], shallowEqual);
// 只订阅当前消息的reaction,而不是整个reactionMessageData
const reactions = useAppSelector((store) => store.reactionMessage[mid], shallowEqual);
const toggleEditMessage = () => {
setEdit((prev) => !prev);
@@ -84,14 +86,18 @@ const Message: FC<IProps> = ({
failed = false,
} = message;
const reactions = reactionMessageData[mid];
const currUser = usersData[fromUid || 0];
// 获取pinInfo中需要的用户信息
const pinInfo = getPinInfo(mid);
const pinCreatorName = useAppSelector((store) =>
pinInfo?.created_by ? store.users.byId[pinInfo.created_by]?.name : undefined,
shallowEqual
);
// if (!message) return null;
let timePrefix = null;
const dayjsTime = dayjs(time);
timePrefix = dayjsTime.isToday() ? "Today" : dayjsTime.isYesterday() ? "Yesterday" : null;
const pinInfo = getPinInfo(mid);
// return null;
const _key = properties?.local_id || mid;
const showExpire = (expires_in ?? 0) > 0;
@@ -143,9 +149,7 @@ const Message: FC<IProps> = ({
pinInfo && "relative",
isSelf && "items-end"
)}
data-pin-tip={`pinned by ${
pinInfo?.created_by ? usersData[pinInfo.created_by]?.name : ""
}`}
data-pin-tip={`pinned by ${pinCreatorName || ""}`}
>
{pinInfo && (
<span
@@ -154,7 +158,7 @@ const Message: FC<IProps> = ({
isSelf ? "right-0" : "left-0"
)}
>
{`pinned by ${pinInfo.created_by ? usersData[pinInfo.created_by]?.name : ""}`}
{`pinned by ${pinCreatorName || ""}`}
</span>
)}
<div
@@ -1,6 +1,4 @@
// @ts-ignore
const CustomList = ({ style, ref, ...props }) => {
// @ts-ignore
const CustomList = ({ style, ref, ...props }: any) => {
return <div style={{ ...style, width: `calc(100% - 2rem)` }} {...props} ref={ref} />;
};
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef, useState, forwardRef, useImperativeHandle } from "react";
import { useCallback, useEffect, useRef, useState, forwardRef, useImperativeHandle, useMemo } from "react";
import { shallowEqual, useDispatch } from "react-redux";
import { Virtuoso, VirtuosoHandle } from "react-virtuoso";
import { useDebounce } from "rooks";
@@ -28,6 +28,8 @@ const VirtualMessageFeed = forwardRef<VirtualMessageFeedHandle, Props>(({ contex
// const { t } = useTranslation("chat");
// const [firstItemIndex, setFirstItemIndex] = useState(firstMsgIndex);
const [atBottom, setAtBottom] = useState(false);
const [visibleCount, setVisibleCount] = useState(100);
const isInitializing = useRef(false);
const [loadMoreMessage, { isLoading: loadingMore, isSuccess, data: historyData }] =
useLazyLoadMoreMessagesQuery();
const vList = useRef<VirtuosoHandle | null>(null);
@@ -40,20 +42,33 @@ const VirtualMessageFeed = forwardRef<VirtualMessageFeedHandle, Props>(({ contex
: store.footprint.historyChannels[id] ?? "",
shallowEqual
);
const mids = useAppSelector(
const allMids = useAppSelector(
(store) =>
context == "dm" ? store.userMessage.byId[id] ?? [] : store.channelMessage[id] ?? [],
shallowEqual
);
const mids = useMemo(() => {
if (allMids.length <= visibleCount) return allMids;
return allMids.slice(-visibleCount);
}, [allMids, visibleCount]);
const selects = useAppSelector(
(store) => store.ui.selectMessages[`${context}_${id}`],
shallowEqual
);
const messageData = useAppSelector((store) => store.message || {}, shallowEqual);
const loginUid = useAppSelector((store) => store.authData.user?.uid, shallowEqual);
const messageData = useAppSelector((store) => store.message, shallowEqual);
const readChannels = useAppSelector((store) => store.footprint.readChannels, shallowEqual);
const readUsers = useAppSelector((store) => store.footprint.readUsers, shallowEqual);
useEffect(() => {
isInitializing.current = true;
setVisibleCount(100);
setTimeout(() => {
isInitializing.current = false;
}, 500);
}, [id]);
useEffect(() => {
if (isSuccess && historyData) {
if (historyData.length == 0) {
@@ -75,14 +90,17 @@ const VirtualMessageFeed = forwardRef<VirtualMessageFeedHandle, Props>(({ contex
// 加载更多
const handleTopStateChange = (isTop: boolean) => {
console.log("reach top ", isTop);
if (isTop) {
if (historyMid === "reached") return;
let lastMid = mids.slice(0, 1)[0];
if (historyMid) {
lastMid = +historyMid;
if (isTop && !isInitializing.current) {
if (allMids.length > visibleCount) {
setVisibleCount(prev => Math.min(prev + 100, allMids.length));
} else {
if (historyMid === "reached") return;
let lastMid = allMids.slice(0, 1)[0];
if (historyMid) {
lastMid = +historyMid;
}
loadMoreMessage({ context, id, mid: lastMid });
}
// prevMids = mids;
loadMoreMessage({ context, id, mid: lastMid });
}
};
// 自动跟随
@@ -101,9 +119,9 @@ const VirtualMessageFeed = forwardRef<VirtualMessageFeedHandle, Props>(({ contex
const handleScrollBottom = useCallback(() => {
const vl = vList!.current;
if (vl) {
vl.scrollToIndex(mids.length - 1);
vl.scrollToIndex(allMids.length - 1);
}
}, [mids]);
}, [allMids]);
const handleBottomStateChange = (bottom: boolean) => {
setAtBottom(bottom);
};
@@ -113,16 +131,44 @@ const VirtualMessageFeed = forwardRef<VirtualMessageFeedHandle, Props>(({ contex
const index = mids.findIndex((m) => m === mid);
if (index !== -1 && vList.current) {
vList.current.scrollToIndex({ index, align: "center", behavior: "smooth" });
} else if (allMids.includes(mid)) {
setVisibleCount(allMids.length);
setTimeout(() => {
const idx = allMids.findIndex((m) => m === mid);
if (idx !== -1 && vList.current) {
vList.current.scrollToIndex({ index: idx, align: "center", behavior: "smooth" });
}
}, 100);
}
}
}));
const readIndex = context == "channel" ? readChannels[id] : readUsers[id];
// 缓存itemContent函数,避免每次都创建新函数
const itemContent = useCallback((idx: number, mid: number) => {
const curr = messageData[mid];
if (!curr) return <div className="w-full h-[1px] invisible"></div>;
const isFirst = idx == 0;
const prev = isFirst ? null : messageData[mids[idx - 1]];
const read = curr?.from_uid == loginUid || mid <= readIndex;
return renderMessageFragment({
selectMode: !!selects,
updateReadIndex: updateReadDebounced,
read,
prev,
curr,
contextId: id,
context
});
}, [messageData, mids, loginUid, readIndex, selects, updateReadDebounced, id, context]);
return (
<>
<Virtuoso
// logLevel={LogLevel.DEBUG}
overscan={50}
increaseViewportBy={{ top: 0, bottom: 400 }}
context={{ loadingMore, id, isChannel: context == "channel" }}
id={`VOCECHAT_FEED_${context}_${id}`}
className="px-1 md:px-4 py-4.5 overflow-x-hidden overflow-y-scroll"
@@ -133,6 +179,7 @@ const VirtualMessageFeed = forwardRef<VirtualMessageFeedHandle, Props>(({ contex
}}
// firstItemIndex={firstItemIndex}
initialTopMostItemIndex={mids.length - 1}
alignToBottom
// startReached={handleLoadMore}
data={mids}
atTopThreshold={context == "channel" ? 160 : 0}
@@ -140,24 +187,7 @@ const VirtualMessageFeed = forwardRef<VirtualMessageFeedHandle, Props>(({ contex
atBottomStateChange={handleBottomStateChange}
atBottomThreshold={400}
followOutput={handleFollowOutput}
itemContent={(idx, mid) => {
// 计算出真正的 index
// const idx = index - firstItemIndex;
const curr = messageData[mid];
if (!curr) return <div className="w-full h-[1px] invisible"></div>;
const isFirst = idx == 0;
const prev = isFirst ? null : messageData[mids[idx - 1]];
const read = curr?.from_uid == loginUid || mid <= readIndex;
return renderMessageFragment({
selectMode: !!selects,
updateReadIndex: updateReadDebounced,
read,
prev,
curr,
contextId: id,
context
});
}}
itemContent={itemContent}
/>
{!atBottom && (
<NewMessageBottomTip context={context} id={id} scrollToBottom={handleScrollBottom} />