perf: reduce redundant chat rerenders

This commit is contained in:
winchen
2026-07-29 11:42:06 +08:00
parent 3694051a95
commit 21e1ae23c1
6 changed files with 209 additions and 83 deletions
+1 -1
View File
@@ -1 +1 @@
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Web IM</title><link rel="icon" href="img/5IuOieu9PF8MJtp43kD2kCkdLVjVwDp7.ico"><script src="config/runtime-config.js"></script><script defer="defer" src="js/chunk-vendors.86756863.js"></script><script defer="defer" src="js/app.5863459a.js"></script><link href="css/app.2c52dabb.css" rel="stylesheet"></head><body><noscript>请启用 JavaScript 后使用 Web IM。</noscript><div id="app"></div></body></html>
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Web IM</title><link rel="icon" href="img/5IuOieu9PF8MJtp43kD2kCkdLVjVwDp7.ico"><script src="config/runtime-config.js"></script><script defer="defer" src="js/chunk-vendors.86756863.js"></script><script defer="defer" src="js/app.84630d8b.js"></script><link href="css/app.2c52dabb.css" rel="stylesheet"></head><body><noscript>请启用 JavaScript 后使用 Web IM。</noscript><div id="app"></div></body></html>
-1
View File
File diff suppressed because one or more lines are too long
+99 -39
View File
@@ -3026,6 +3026,12 @@ const IMAGE_MAX_SCALE = 6;
y: 0,
message: null
});
const messageTimeFormatter = new Intl.DateTimeFormat('zh-CN', {
hour: '2-digit',
minute: '2-digit',
hour12: false,
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
});
const imageViewer = (0,vue__WEBPACK_IMPORTED_MODULE_8__/* .reactive */ .Kh)({
visible: false,
url: '',
@@ -3125,20 +3131,20 @@ const IMAGE_MAX_SCALE = 6;
});
function scrollToBottom() {
(0,vue__WEBPACK_IMPORTED_MODULE_6__/* .nextTick */ .dY)(() => {
if (listEl.value) {
const scroll = () => {
if (listEl.value) {
listEl.value.scrollTop = listEl.value.scrollHeight;
}
};
scroll();
requestAnimationFrame(() => {
scroll();
requestAnimationFrame(scroll);
});
}
requestAnimationFrame(() => {
if (listEl.value) {
listEl.value.scrollTop = listEl.value.scrollHeight;
}
});
});
}
function isNearBottom() {
const element = listEl.value;
if (!element) {
return true;
}
return element.scrollHeight - element.scrollTop - element.clientHeight < 120;
}
function scrollToMessage(messageId) {
;(0,vue__WEBPACK_IMPORTED_MODULE_6__/* .nextTick */ .dY)(() => {
if (!listEl.value || !messageId) {
@@ -3153,7 +3159,11 @@ const IMAGE_MAX_SCALE = 6;
}
});
}
;(0,vue__WEBPACK_IMPORTED_MODULE_6__/* .watch */ .wB)(() => props.messages.map(message => `${message.id}:${message.status}:${message.deliveryStatus || ''}:${message.messageUid || ''}:${message.recalled ? '1' : ''}`).join('|'), scrollToBottom, {
;(0,vue__WEBPACK_IMPORTED_MODULE_6__/* .watch */ .wB)(() => props.messages.map(messageIdentity).join('|'), (nextKeys, previousKeys) => {
if (!previousKeys || nextKeys !== previousKeys && isNearBottom()) {
scrollToBottom();
}
}, {
immediate: true
});
(0,vue__WEBPACK_IMPORTED_MODULE_6__/* .watch */ .wB)(() => props.attentionMessageId, scrollToMessage);
@@ -3507,12 +3517,7 @@ const IMAGE_MAX_SCALE = 6;
closeMenu();
}
function formatTime(timestamp) {
return new Intl.DateTimeFormat('zh-CN', {
hour: '2-digit',
minute: '2-digit',
hour12: false,
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
}).format(timestamp);
return messageTimeFormatter.format(timestamp);
}
function deliveryState(message) {
if (message.direction !== 'out' || message.status === 'failed' || message.status === 'recalled' || message.recalled) {
@@ -7653,6 +7658,25 @@ const useContactStore = (0,pinia__WEBPACK_IMPORTED_MODULE_6__/* .defineStore */
const LOCAL_CONVERSATION_STORAGE_KEY = 'qmt-web-im-local-conversations-v2';
const LOCAL_FAVORITE_MESSAGES_KEY = 'qmt-web-im-favorite-messages-v1';
const recentRealtimeNotifications = new Map();
function isDuplicateRealtimeNotification(payload) {
const messageId = String(payload?.messageId || '');
if (!messageId) {
return false;
}
const key = `${payload?.kind || 'message'}:${messageId}`;
const now = Date.now();
const previousAt = Number(recentRealtimeNotifications.get(key) || 0);
recentRealtimeNotifications.set(key, now);
if (recentRealtimeNotifications.size > 200) {
recentRealtimeNotifications.forEach((seenAt, seenKey) => {
if (now - seenAt > 30000) {
recentRealtimeNotifications.delete(seenKey);
}
});
}
return now - previousAt < 5000;
}
function scopedStorageKey(baseKey) {
const session = (0,_stores_session__WEBPACK_IMPORTED_MODULE_22__/* .useSessionStore */ .B)();
const scope = `${_config__WEBPACK_IMPORTED_MODULE_21__/* .appConfig */ .U.appServer || ''}:${session.userId || 'anonymous'}`;
@@ -7693,6 +7717,16 @@ function messageStableKey(message) {
function messageActivityTimestamp(message) {
return Number(message?.activityTimestamp || message?.editedAt || message?.displayTimestamp || message?.timestamp || 0);
}
function hasSameRenderState(left, right) {
if (left === right) {
return true;
}
try {
return JSON.stringify(left) === JSON.stringify(right);
} catch (error) {
return false;
}
}
function isRecalledMessage(message) {
return Boolean(message?.recalled) || message?.status === 'recalled';
}
@@ -8229,6 +8263,7 @@ const useConversationStore = (0,pinia__WEBPACK_IMPORTED_MODULE_19__/* .defineSto
if (!remote.length && this.conversations.length) {
return;
}
const previousActiveId = this.activeId;
const merged = new Map();
this.conversations.forEach(conversation => {
merged.set(conversation.id, conversation);
@@ -8241,7 +8276,11 @@ const useConversationStore = (0,pinia__WEBPACK_IMPORTED_MODULE_19__/* .defineSto
unread
});
});
this.conversations = Array.from(merged.values()).map(sanitizeConversation).sort((a, b) => Number(b.updatedAt || 0) - Number(a.updatedAt || 0)).slice(0, 100);
const nextConversations = Array.from(merged.values()).map(sanitizeConversation).sort((a, b) => Number(b.updatedAt || 0) - Number(a.updatedAt || 0)).slice(0, 100);
const conversationsChanged = !hasSameRenderState(this.conversations, nextConversations);
if (conversationsChanged) {
this.conversations = nextConversations;
}
if (!this.conversations.length) {
this.activeId = '';
this.messagesByConversation = {};
@@ -8253,8 +8292,12 @@ const useConversationStore = (0,pinia__WEBPACK_IMPORTED_MODULE_19__/* .defineSto
if (!this.conversations.find(item => item.id === this.activeId)) {
this.activeId = this.conversations[0].id;
}
writeLocalConversations(this.conversations);
this.loadActiveMessages();
if (conversationsChanged) {
writeLocalConversations(this.conversations);
}
if (previousActiveId !== this.activeId || !this.messagesByConversation[this.activeId]) {
this.loadActiveMessages();
}
},
appendMessage(conversationId, message) {
message = normalizeMessageForDisplay(message);
@@ -8362,10 +8405,14 @@ const useConversationStore = (0,pinia__WEBPACK_IMPORTED_MODULE_19__/* .defineSto
this.syncVisibleMessages(conversationId);
},
syncVisibleMessages(conversationId) {
this.visibleMessagesByConversation = {
...this.visibleMessagesByConversation,
[conversationId]: mergeVisibleMessages(this.optimisticMessagesByConversation[conversationId] || [], this.messagesByConversation[conversationId] || [])
};
const currentMessages = this.visibleMessagesByConversation[conversationId] || [];
const nextMessages = mergeVisibleMessages(this.optimisticMessagesByConversation[conversationId] || [], this.messagesByConversation[conversationId] || []);
if (!hasSameRenderState(currentMessages, nextMessages)) {
this.visibleMessagesByConversation = {
...this.visibleMessagesByConversation,
[conversationId]: nextMessages
};
}
},
keepOptimisticMessage(conversationId, message) {
if (!this.optimisticMessagesByConversation[conversationId]) {
@@ -8417,10 +8464,13 @@ const useConversationStore = (0,pinia__WEBPACK_IMPORTED_MODULE_19__/* .defineSto
this.loading = true;
const messages = _services_imClient__WEBPACK_IMPORTED_MODULE_20__/* .imClient */ .wD.getMessages(conversation, 30);
this.pruneOptimisticMessages(conversation.id, messages);
this.messagesByConversation = {
...this.messagesByConversation,
[conversation.id]: mergeVisibleMessages(this.messagesByConversation[conversation.id] || [], messages)
};
const nextMessages = mergeVisibleMessages(this.messagesByConversation[conversation.id] || [], messages);
if (!hasSameRenderState(this.messagesByConversation[conversation.id] || [], nextMessages)) {
this.messagesByConversation = {
...this.messagesByConversation,
[conversation.id]: nextMessages
};
}
this.syncVisibleMessages(conversation.id);
this.loading = false;
this.syncActiveMessages(syncOptions);
@@ -8462,10 +8512,13 @@ const useConversationStore = (0,pinia__WEBPACK_IMPORTED_MODULE_19__/* .defineSto
}
const editedIncomingMessages = incomingEditedMessages(previousMessages, messages);
this.pruneOptimisticMessages(syncedConversationId, messages);
this.messagesByConversation = {
...this.messagesByConversation,
[syncedConversationId]: mergeVisibleMessages(this.messagesByConversation[syncedConversationId] || [], messages)
};
const nextMessages = mergeVisibleMessages(this.messagesByConversation[syncedConversationId] || [], messages);
if (!hasSameRenderState(this.messagesByConversation[syncedConversationId] || [], nextMessages)) {
this.messagesByConversation = {
...this.messagesByConversation,
[syncedConversationId]: nextMessages
};
}
this.syncVisibleMessages(syncedConversationId);
if (editedIncomingMessages.length && syncedConversationId === this.activeId) {
this.focusMessage(editedIncomingMessages.sort((a, b) => messageActivityTimestamp(b) - messageActivityTimestamp(a))[0]);
@@ -8478,9 +8531,13 @@ const useConversationStore = (0,pinia__WEBPACK_IMPORTED_MODULE_19__/* .defineSto
return latestMessage;
}, null);
if (latest) {
conversation.subtitle = messagePreviewText(latest);
conversation.updatedAt = messageActivityTimestamp(latest);
writeLocalConversations(this.conversations);
const nextSubtitle = messagePreviewText(latest);
const nextUpdatedAt = messageActivityTimestamp(latest);
if (conversation.subtitle !== nextSubtitle || Number(conversation.updatedAt || 0) !== nextUpdatedAt) {
conversation.subtitle = nextSubtitle;
conversation.updatedAt = nextUpdatedAt;
writeLocalConversations(this.conversations);
}
}
} catch (error) {
// Keep the local view usable if a polling request misses.
@@ -8536,7 +8593,10 @@ const useConversationStore = (0,pinia__WEBPACK_IMPORTED_MODULE_19__/* .defineSto
this.refreshRemoteConversations();
}
},
handleRealtimeNotification() {
handleRealtimeNotification(payload) {
if (isDuplicateRealtimeNotification(payload)) {
return;
}
window.clearTimeout(this.realtimeSyncTimer);
this.realtimeSyncTimer = window.setTimeout(() => {
this.refreshRemoteConversations();
@@ -9749,4 +9809,4 @@ const __exports__ = _NavigationRail_vue_vue_type_script_setup_true_lang_js__WEBP
/******/
/******/ })()
;
//# sourceMappingURL=app.5863459a.js.map
//# sourceMappingURL=app.84630d8b.js.map
+1
View File
File diff suppressed because one or more lines are too long
+26 -20
View File
@@ -259,6 +259,12 @@ const contextMenu = reactive({
});
const IMAGE_MIN_SCALE = 0.5;
const IMAGE_MAX_SCALE = 6;
const messageTimeFormatter = new Intl.DateTimeFormat('zh-CN', {
hour: '2-digit',
minute: '2-digit',
hour12: false,
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
});
const imageViewer = reactive({
visible: false,
url: '',
@@ -332,21 +338,22 @@ const menuItems = computed(() => {
function scrollToBottom() {
nextTick(() => {
if (listEl.value) {
const scroll = () => {
if (listEl.value) {
listEl.value.scrollTop = listEl.value.scrollHeight;
}
};
scroll();
requestAnimationFrame(() => {
scroll();
requestAnimationFrame(scroll);
});
}
requestAnimationFrame(() => {
if (listEl.value) {
listEl.value.scrollTop = listEl.value.scrollHeight;
}
});
});
}
function isNearBottom() {
const element = listEl.value;
if (!element) {
return true;
}
return element.scrollHeight - element.scrollTop - element.clientHeight < 120;
}
function scrollToMessage(messageId) {
nextTick(() => {
if (!listEl.value || !messageId) {
@@ -361,8 +368,12 @@ function scrollToMessage(messageId) {
}
watch(
() => props.messages.map(message => `${message.id}:${message.status}:${message.deliveryStatus || ''}:${message.messageUid || ''}:${message.recalled ? '1' : ''}`).join('|'),
scrollToBottom,
() => props.messages.map(messageIdentity).join('|'),
(nextKeys, previousKeys) => {
if (!previousKeys || (nextKeys !== previousKeys && isNearBottom())) {
scrollToBottom();
}
},
{ immediate: true }
);
@@ -762,12 +773,7 @@ function chooseMenuAction(action) {
}
function formatTime(timestamp) {
return new Intl.DateTimeFormat('zh-CN', {
hour: '2-digit',
minute: '2-digit',
hour12: false,
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
}).format(timestamp);
return messageTimeFormatter.format(timestamp);
}
function deliveryState(message) {
+82 -22
View File
@@ -6,6 +6,26 @@ import { realtimeNotifications } from '@/services/realtimeNotifications';
const LOCAL_CONVERSATION_STORAGE_KEY = 'qmt-web-im-local-conversations-v2';
const LOCAL_FAVORITE_MESSAGES_KEY = 'qmt-web-im-favorite-messages-v1';
const recentRealtimeNotifications = new Map();
function isDuplicateRealtimeNotification(payload) {
const messageId = String(payload?.messageId || '');
if (!messageId) {
return false;
}
const key = `${payload?.kind || 'message'}:${messageId}`;
const now = Date.now();
const previousAt = Number(recentRealtimeNotifications.get(key) || 0);
recentRealtimeNotifications.set(key, now);
if (recentRealtimeNotifications.size > 200) {
recentRealtimeNotifications.forEach((seenAt, seenKey) => {
if (now - seenAt > 30000) {
recentRealtimeNotifications.delete(seenKey);
}
});
}
return now - previousAt < 5000;
}
function scopedStorageKey(baseKey) {
const session = useSessionStore();
@@ -56,6 +76,17 @@ function messageActivityTimestamp(message) {
return Number(message?.activityTimestamp || message?.editedAt || message?.displayTimestamp || message?.timestamp || 0);
}
function hasSameRenderState(left, right) {
if (left === right) {
return true;
}
try {
return JSON.stringify(left) === JSON.stringify(right);
} catch (error) {
return false;
}
}
function isRecalledMessage(message) {
return Boolean(message?.recalled) || message?.status === 'recalled';
}
@@ -616,6 +647,7 @@ export const useConversationStore = defineStore('conversations', {
return;
}
const previousActiveId = this.activeId;
const merged = new Map();
this.conversations.forEach(conversation => {
merged.set(conversation.id, conversation);
@@ -629,10 +661,14 @@ export const useConversationStore = defineStore('conversations', {
});
});
this.conversations = Array.from(merged.values())
const nextConversations = Array.from(merged.values())
.map(sanitizeConversation)
.sort((a, b) => Number(b.updatedAt || 0) - Number(a.updatedAt || 0))
.slice(0, 100);
const conversationsChanged = !hasSameRenderState(this.conversations, nextConversations);
if (conversationsChanged) {
this.conversations = nextConversations;
}
if (!this.conversations.length) {
this.activeId = '';
@@ -646,8 +682,12 @@ export const useConversationStore = defineStore('conversations', {
if (!this.conversations.find(item => item.id === this.activeId)) {
this.activeId = this.conversations[0].id;
}
writeLocalConversations(this.conversations);
this.loadActiveMessages();
if (conversationsChanged) {
writeLocalConversations(this.conversations);
}
if (previousActiveId !== this.activeId || !this.messagesByConversation[this.activeId]) {
this.loadActiveMessages();
}
},
appendMessage(conversationId, message) {
message = normalizeMessageForDisplay(message);
@@ -755,13 +795,17 @@ export const useConversationStore = defineStore('conversations', {
this.syncVisibleMessages(conversationId);
},
syncVisibleMessages(conversationId) {
this.visibleMessagesByConversation = {
...this.visibleMessagesByConversation,
[conversationId]: mergeVisibleMessages(
this.optimisticMessagesByConversation[conversationId] || [],
this.messagesByConversation[conversationId] || []
)
};
const currentMessages = this.visibleMessagesByConversation[conversationId] || [];
const nextMessages = mergeVisibleMessages(
this.optimisticMessagesByConversation[conversationId] || [],
this.messagesByConversation[conversationId] || []
);
if (!hasSameRenderState(currentMessages, nextMessages)) {
this.visibleMessagesByConversation = {
...this.visibleMessagesByConversation,
[conversationId]: nextMessages
};
}
},
keepOptimisticMessage(conversationId, message) {
if (!this.optimisticMessagesByConversation[conversationId]) {
@@ -813,10 +857,13 @@ export const useConversationStore = defineStore('conversations', {
this.loading = true;
const messages = imClient.getMessages(conversation, 30);
this.pruneOptimisticMessages(conversation.id, messages);
this.messagesByConversation = {
...this.messagesByConversation,
[conversation.id]: mergeVisibleMessages(this.messagesByConversation[conversation.id] || [], messages)
};
const nextMessages = mergeVisibleMessages(this.messagesByConversation[conversation.id] || [], messages);
if (!hasSameRenderState(this.messagesByConversation[conversation.id] || [], nextMessages)) {
this.messagesByConversation = {
...this.messagesByConversation,
[conversation.id]: nextMessages
};
}
this.syncVisibleMessages(conversation.id);
this.loading = false;
this.syncActiveMessages(syncOptions);
@@ -857,10 +904,16 @@ export const useConversationStore = defineStore('conversations', {
}
const editedIncomingMessages = incomingEditedMessages(previousMessages, messages);
this.pruneOptimisticMessages(syncedConversationId, messages);
this.messagesByConversation = {
...this.messagesByConversation,
[syncedConversationId]: mergeVisibleMessages(this.messagesByConversation[syncedConversationId] || [], messages)
};
const nextMessages = mergeVisibleMessages(
this.messagesByConversation[syncedConversationId] || [],
messages
);
if (!hasSameRenderState(this.messagesByConversation[syncedConversationId] || [], nextMessages)) {
this.messagesByConversation = {
...this.messagesByConversation,
[syncedConversationId]: nextMessages
};
}
this.syncVisibleMessages(syncedConversationId);
if (editedIncomingMessages.length && syncedConversationId === this.activeId) {
this.focusMessage(editedIncomingMessages.sort((a, b) => messageActivityTimestamp(b) - messageActivityTimestamp(a))[0]);
@@ -873,9 +926,13 @@ export const useConversationStore = defineStore('conversations', {
return latestMessage;
}, null);
if (latest) {
conversation.subtitle = messagePreviewText(latest);
conversation.updatedAt = messageActivityTimestamp(latest);
writeLocalConversations(this.conversations);
const nextSubtitle = messagePreviewText(latest);
const nextUpdatedAt = messageActivityTimestamp(latest);
if (conversation.subtitle !== nextSubtitle || Number(conversation.updatedAt || 0) !== nextUpdatedAt) {
conversation.subtitle = nextSubtitle;
conversation.updatedAt = nextUpdatedAt;
writeLocalConversations(this.conversations);
}
}
} catch (error) {
// Keep the local view usable if a polling request misses.
@@ -931,7 +988,10 @@ export const useConversationStore = defineStore('conversations', {
this.refreshRemoteConversations();
}
},
handleRealtimeNotification() {
handleRealtimeNotification(payload) {
if (isDuplicateRealtimeNotification(payload)) {
return;
}
window.clearTimeout(this.realtimeSyncTimer);
this.realtimeSyncTimer = window.setTimeout(() => {
this.refreshRemoteConversations();