From 7f14b4b65edf0d26c2bca2ad07a98a9ad893e116 Mon Sep 17 00:00:00 2001 From: haorwen Date: Wed, 31 Dec 2025 00:15:36 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E4=BC=98=E5=8C=96=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E5=8A=A0=E8=BD=BD=20(#288)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- config/webpack.config.js | 2 +- package.json | 2 +- src/app/services/message.ts | 21 ++++- src/components/Message/index.tsx | 22 +++-- .../Layout/VirtualMessageFeed/CustomList.tsx | 4 +- .../chat/Layout/VirtualMessageFeed/index.tsx | 90 ++++++++++++------- 6 files changed, 94 insertions(+), 47 deletions(-) diff --git a/config/webpack.config.js b/config/webpack.config.js index 3acbaa79..8fca5dd8 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -261,7 +261,7 @@ module.exports = function (webpackEnv) { // Handle node_modules packages that contain sourcemaps shouldUseSourceMap && { enforce: "pre", - exclude: /@babel(?:\/|\\{1,2})runtime/, + exclude: /@babel(?:\/|\\{1,2})runtime|@toast-ui\+editor/, test: /\.(js|mjs|jsx|ts|tsx|css)$/, loader: require.resolve("source-map-loader") }, diff --git a/package.json b/package.json index 09afca32..3c19f0cd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vocechat-web", - "version": "0.9.43", + "version": "0.9.44", "homepage": "https://voce.chat", "dependencies": { "@metamask/onboarding": "^1.0.1", diff --git a/src/app/services/message.ts b/src/app/services/message.ts index 2e953c07..9954306b 100644 --- a/src/app/services/message.ts +++ b/src/app/services/message.ts @@ -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); } } }), diff --git a/src/components/Message/index.tsx b/src/components/Message/index.tsx index 160c8668..0ac1bcbf 100644 --- a/src/components/Message/index.tsx +++ b/src/components/Message/index.tsx @@ -49,8 +49,10 @@ const Message: FC = ({ 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 = ({ 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 = ({ 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 && ( = ({ isSelf ? "right-0" : "left-0" )} > - {`pinned by ${pinInfo.created_by ? usersData[pinInfo.created_by]?.name : ""}`} + {`pinned by ${pinCreatorName || ""}`} )}
{ - // @ts-ignore +const CustomList = ({ style, ref, ...props }: any) => { return
; }; diff --git a/src/routes/chat/Layout/VirtualMessageFeed/index.tsx b/src/routes/chat/Layout/VirtualMessageFeed/index.tsx index c3732124..825f90b3 100644 --- a/src/routes/chat/Layout/VirtualMessageFeed/index.tsx +++ b/src/routes/chat/Layout/VirtualMessageFeed/index.tsx @@ -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(({ 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(null); @@ -40,20 +42,33 @@ const VirtualMessageFeed = forwardRef(({ 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(({ 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(({ 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(({ 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
; + 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 ( <> (({ contex }} // firstItemIndex={firstItemIndex} initialTopMostItemIndex={mids.length - 1} + alignToBottom // startReached={handleLoadMore} data={mids} atTopThreshold={context == "channel" ? 160 : 0} @@ -140,24 +187,7 @@ const VirtualMessageFeed = forwardRef(({ contex atBottomStateChange={handleBottomStateChange} atBottomThreshold={400} followOutput={handleFollowOutput} - itemContent={(idx, mid) => { - // 计算出真正的 index - // const idx = index - firstItemIndex; - const curr = messageData[mid]; - if (!curr) return
; - 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 && (