Improved `printMessages` performance on large chats by reducing DOM updates. (#4947) * Improved printMessages performance by reducing DOM updates. Refactored part of addOneMessage into createMessageElement. Before: Rendered 1000 messages in 25.529199999809265 seconds. After: Rendered 1000 messages in 5.088 seconds. * Fixed mistakes: https://github.com/SillyTavern/SillyTavern/pull/4947#pullrequestreview-3624592007 * Formatting fix * Fixed scroll and last_mes order.. * Refactored printMessages logic into redisplayChat. * Passed `fade`. * Cleanup. * WIP. Removed getMessageFromTemplate Refactored addOneMessage and updateMessageElement. * Minor changes to better match https://github.com/SillyTavern/SillyTavern/pull/4985 * If insert is false, `newMessage` may not be new, so I renamed `newMessage` to `messageElement` and `newMessageId` to `messageId`. I also rearranged some constants to improve readability. * Accept 0 as valid insertAfter/insertBefore targets * Renamed forceId to messageId for clarity. * Change forceId to undefined by default. * Swapped `forceId` to `messageId` in addOneMessage. * Added adjustMediaScroll to updateMessageElement. * Revert "Change forceId to undefined by default." This reverts commit cbda7eb3fe0a2aa46e0988e82260910c62bc5034. --------- Co-authored-by: user <user@exmaple.com> Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
Signed| @@ -1397,7 +1397,7 @@ export async function showMoreMessages(messagesToLoad = null) { | ||
| 1397 | 1397 | const firstId = clamp(messageId - count, 0, Infinity); |
| 1398 | 1398 | const messageElements = []; |
| 1399 | 1399 | chat.slice(firstId, messageId).forEach((message, id) => { |
| 1400 | 1400 | messageElements.push(addOneMessageupdateMessageElement(message, { scroll: false, forceIdmessageId: firstId + id, showSwipes: false, insert: false })); |
| 1401 | 1401 | }); |
| 1402 | 1402 | // This could be faster: https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentElement |
| 1403 | 1403 | // Fallback to chatElement if the button isn't where it's expected to be. |
| @@ -1458,7 +1458,7 @@ export async function redisplayChat({ targetChat = chat, startIndex = 0, fade = | ||
| 1458 | 1458 | if (messages.length > 0) { |
| 1459 | 1459 | const newMessageElements = messages.map((message, offset) => { |
| 1460 | 1460 | const i = startIndex + offset; |
| 1461 | 1461 | const messageElement = addOneMessageupdateMessageElement(message, { scroll: false, forceIdmessageId: i, showSwipes: false, insert: false }); |
| 1462 | 1462 | |
| 1463 | 1463 | return messageElement[0]; |
| 1464 | 1464 | }); |
| @@ -2430,16 +2430,15 @@ function getMessageTextHTML(message, { messageId = chat.indexOf(message) }) { | ||
| 2430 | 2430 | * Adds a single message to the chat. |
| 2431 | 2431 | * @param {ChatMessage} mes Message object |
| 2432 | 2432 | * @param {object} [options] Options |
| 2433 | 2433 | * @param {string} [options.type=undefined|'normalswipe'] MessageDeprecated. typeUse updateMessageElement instead. |
| 2434 | 2434 | * @param {number} [options.insertAfter=null] Message ID to insert the new message after |
| 2435 | 2435 | * @param {boolean} [options.scroll=true] Whether to scroll to the new message |
| 2436 | 2436 | * @param {number} [options.insertBefore=null] Message ID to insert the new message before |
| 2437 | 2437 | * @param {number} [options.forceId=null] Force the message ID |
| 2438 | 2438 | * @param {boolean} [options.showSwipes=true] Whether to refresh the swipe buttons. |
| 2439 | - * @param {boolean} [options.insert=true] Whether to insert the message into the DOM. | |
| 2440 | 2439 | * @returns {JQuery<HTMLElement>} The newly added message element |
| 2441 | 2440 | */ |
| 2442 | 2441 | export function addOneMessage(mes, { type = 'normal'undefined, insertAfter = null, scroll = true, insertBefore = null, forceId = null, showSwipes = true, insert = true } = {}) { |
| 2443 | 2442 | // Callers push the new message to chat before calling addOneMessage |
| 2444 | 2443 | const messageId = (() => { |
| 2445 | 2444 | if (typeof forceId === 'number') { |
| @@ -2458,6 +2457,55 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll | ||
| 2458 | 2457 | return chat.length - 1; |
| 2459 | 2458 | })(); |
| 2460 | 2459 | |
| 2460 | + let messageElement; | |
| 2461 | + | |
| 2462 | + if (type === 'swipe') { | |
| 2463 | + // Forbidden black magic | |
| 2464 | + // This allows to use "continue" on user messages | |
| 2465 | + mes.swipe_id ??= 0; | |
| 2466 | + mes.swipes ??= [mes.mes]; | |
| 2467 | + //This keeps listeners intact. | |
| 2468 | + messageElement = chatElement.find(`[mesid="${messageId}"]`); | |
| 2469 | + updateMessageElement(mes, { messageId, messageElement, adjustMediaScroll: scroll ? SCROLL_BEHAVIOR.ADJUST : SCROLL_BEHAVIOR.NONE }); | |
| 2470 | + } else { | |
| 2471 | + messageElement = updateMessageElement(mes, { messageId, adjustMediaScroll: scroll ? SCROLL_BEHAVIOR.ADJUST : SCROLL_BEHAVIOR.NONE }); | |
| 2472 | + if (typeof insertAfter === 'number' && insertAfter >= 0) { | |
| 2473 | + const target = chatElement.find(`.mes[mesid="${insertAfter}"]`); | |
| 2474 | + $(messageElement).insertAfter(target); | |
| 2475 | + } else if (typeof insertBefore === 'number' && insertBefore >= 0) { | |
| 2476 | + const target = chatElement.find(`.mes[mesid="${insertBefore}"]`); | |
| 2477 | + $(messageElement).insertBefore(target); | |
| 2478 | + } else { | |
| 2479 | + chatElement.append(messageElement); | |
| 2480 | + } | |
| 2481 | + } | |
| 2482 | + | |
| 2483 | + | |
| 2484 | + //last_mes should always be updated. | |
| 2485 | + chatElement.find('.mes').removeClass('last_mes'); | |
| 2486 | + chatElement.find('.mes').last().addClass('last_mes'); | |
| 2487 | + | |
| 2488 | + if (showSwipes) refreshSwipeButtons(); | |
| 2489 | + // Don't scroll if not inserting last | |
| 2490 | + if (!insertAfter && !insertBefore && scroll) { | |
| 2491 | + scrollChatToBottom({ waitForFrame: true }); | |
| 2492 | + } | |
| 2493 | + | |
| 2494 | + applyCharacterTagsToMessageDivs({ mesIds: messageId }); | |
| 2495 | + updateEditArrowClasses(); | |
| 2496 | + return messageElement; | |
| 2497 | +} | |
| 2498 | + | |
| 2499 | +/** | |
| 2500 | + * Creates the element of a single message as if it were the last message or at forceMesId | |
| 2501 | + * @param {ChatMessage} mes Message object | |
| 2502 | + * @param {object} [options] Options | |
| 2503 | + * @param {number} [options.messageId=chat.length - 1] Force the message ID | |
| 2504 | + * @param {JQuery<HTMLElement>} [options.messageElement=messageTemplate.clone()] This message element will be updated with the ChatMessage object. | |
| 2505 | + * @param {SCROLL_BEHAVIOR} [options.adjustMediaScroll=SCROLL_BEHAVIOR.NONE] Scroll behavior option passed to appendMediaToMessage. | |
| 2506 | + * @returns {JQuery<HTMLElement>} Rendered HTMLElement. | |
| 2507 | + */ | |
| 2508 | +export function updateMessageElement(mes, { messageId = chat.length - 1, messageElement = messageTemplate.clone(), adjustMediaScroll = SCROLL_BEHAVIOR.NONE } = {}) { | |
| 2461 | 2509 | |
| 2462 | 2510 | let avatarImg = getThumbnailUrl('persona', user_avatar); |
| 2463 | 2511 | |
| @@ -2487,19 +2535,6 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll | ||
| 2487 | 2535 | const tokenCount = mes.extra?.token_count; |
| 2488 | 2536 | const { timerValue, timerTitle } = formatGenerationTimer(mes.gen_started, mes.gen_finished, mes.extra?.token_count, mes.extra?.reasoning_duration, mes.extra?.time_to_first_token); |
| 2489 | 2537 | |
| 2490 | - let messageElement; | |
| 2491 | - | |
| 2492 | - if (type === 'swipe') { | |
| 2493 | - // Forbidden black magic | |
| 2494 | - // This allows to use "continue" on user messages | |
| 2495 | - mes.swipe_id ??= 0; | |
| 2496 | - mes.swipes ??= [mes.mes]; | |
| 2497 | - //This keeps listeners intact. | |
| 2498 | - messageElement = chatElement.find(`[mesid="${messageId}"]`); | |
| 2499 | - } else { | |
| 2500 | - messageElement = messageTemplate.clone(); | |
| 2501 | - } | |
| 2502 | - | |
| 2503 | 2538 | messageElement.attr({ |
| 2504 | 2539 | 'mesid': messageId, |
| 2505 | 2540 | 'swipeid': mes.swipe_id ?? 0, |
| @@ -2533,18 +2568,6 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll | ||
| 2533 | 2568 | insertSVGIcon(messageElement, mes.extra); |
| 2534 | 2569 | } |
| 2535 | 2570 | |
| 2536 | - if (type !== 'swipe' && insert) { | |
| 2537 | - if (typeof insertAfter === 'number' && insertAfter >= 0) { | |
| 2538 | - const target = chatElement.find(`.mes[mesid="${insertAfter}"]`); | |
| 2539 | - $(messageElement).insertAfter(target); | |
| 2540 | - } else if (typeof insertBefore === 'number' && insertBefore >= 0) { | |
| 2541 | - const target = chatElement.find(`.mes[mesid="${insertBefore}"]`); | |
| 2542 | - $(messageElement).insertBefore(target); | |
| 2543 | - } else { | |
| 2544 | - chatElement.append(messageElement); | |
| 2545 | - } | |
| 2546 | - } | |
| 2547 | - | |
| 2548 | 2571 | if (mes?.extra?.isSmallSys === true) { |
| 2549 | 2572 | messageElement.addClass('smallSysMes'); |
| 2550 | 2573 | } |
| @@ -2560,7 +2583,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll | ||
| 2560 | 2583 | $(this).parent().html('<div class="missing-avatar fa-solid fa-user-slash"></div>'); |
| 2561 | 2584 | }); |
| 2562 | 2585 | |
| 2563 | 2586 | appendMediaToMessage(mes, messageElement, scroll ? SCROLL_BEHAVIOR.ADJUST : SCROLL_BEHAVIOR.NONEadjustMediaScroll); |
| 2564 | 2587 | messageElement.find('.mes_text').html(messageHTML); |
| 2565 | 2588 | addCopyToCodeBlocks(messageElement); |
| 2566 | 2589 | |
| @@ -2569,26 +2592,6 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll | ||
| 2569 | 2592 | updateSwipeCounter(messageId, { message: mes, messageElement }); |
| 2570 | 2593 | } |
| 2571 | 2594 | |
| 2572 | - // The caller should handle the rest after adding a message to DOM. | |
| 2573 | - if (!insert) { | |
| 2574 | - return messageElement; | |
| 2575 | - } | |
| 2576 | - | |
| 2577 | - //last_mes should always be updated. | |
| 2578 | - chatElement.find('.mes').removeClass('last_mes'); | |
| 2579 | - chatElement.find('.mes').last().addClass('last_mes'); | |
| 2580 | - if (showSwipes) { | |
| 2581 | - refreshSwipeButtons(); | |
| 2582 | - } | |
| 2583 | - | |
| 2584 | - // Don't scroll if not inserting last | |
| 2585 | - if (!insertAfter && !insertBefore && scroll) { | |
| 2586 | - scrollChatToBottom({ waitForFrame: true }); | |
| 2587 | - } | |
| 2588 | - | |
| 2589 | - applyCharacterTagsToMessageDivs({ mesIds: messageId }); | |
| 2590 | - updateEditArrowClasses(); | |
| 2591 | - | |
| 2592 | 2595 | return messageElement; |
| 2593 | 2596 | } |
| 2594 | 2597 | |
| @@ -11703,7 +11706,7 @@ jQuery(async function () { | ||
| 11703 | 11706 | } |
| 11704 | 11707 | |
| 11705 | 11708 | chat.splice(Number(this_edit_mes_id) + 1, 0, clone); |
| 11706 | 11709 | const newMessageElement = addOneMessageupdateMessageElement(clone, { insert: false }); |
| 11707 | 11710 | this_edit_mes_element.after(newMessageElement); |
| 11708 | 11711 | |
| 11709 | 11712 | updateViewMessageIds(); |
| @@ -54,6 +54,7 @@ import { | ||
| 54 | 54 | system_avatar, |
| 55 | 55 | system_message_types, |
| 56 | 56 | this_chid, |
| 57 | + updateMessageElement, | |
| 57 | 58 | } from '../script.js'; |
| 58 | 59 | import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; |
| 59 | 60 | import { SlashCommandParserError } from './slash-commands/SlashCommandParserError.js'; |
| @@ -4642,7 +4643,7 @@ async function messageRoleCallback(args, role) { | ||
| 4642 | 4643 | await eventSource.emit(event_types.MESSAGE_EDITED, modifyAt); |
| 4643 | 4644 | const existingMessage = chatElement.find(`.mes[mesid="${modifyAt}"]`); |
| 4644 | 4645 | if (existingMessage.length) { |
| 4645 | 4646 | const newMessageElement = addOneMessageupdateMessageElement(message, { forceIdmessageId: modifyAt, insert: false, scroll: false }); |
| 4646 | 4647 | existingMessage.after(newMessageElement); |
| 4647 | 4648 | existingMessage.remove(); |
| 4648 | 4649 | } |
| @@ -4708,7 +4709,7 @@ async function messageNameCallback(args, name) { | ||
| 4708 | 4709 | await eventSource.emit(event_types.MESSAGE_EDITED, modifyAt); |
| 4709 | 4710 | const existingMessage = chatElement.find(`.mes[mesid="${modifyAt}"]`); |
| 4710 | 4711 | if (existingMessage.length) { |
| 4711 | 4712 | const newMessageElement = addOneMessageupdateMessageElement(message, { forceIdmessageId: modifyAt, insert: false, scroll: false }); |
| 4712 | 4713 | existingMessage.after(newMessageElement); |
| 4713 | 4714 | existingMessage.remove(); |
| 4714 | 4715 | } |