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>

7d9954082955c17c2a46e26dbe8146a9449af896

DeclineThyself <235079501+DeclineThyself@users.noreply.github.com>

Signed
2 files changed, +58 -54Ignore whitespace
public/script.js+55 -52
@@ -1397,7 +1397,7 @@ export async function showMoreMessages(messagesToLoad = null) {
1397 const firstId = clamp(messageId - count, 0, Infinity);1397 const firstId = clamp(messageId - count, 0, Infinity);
1398 const messageElements = [];1398 const messageElements = [];
1399 chat.slice(firstId, messageId).forEach((message, id) => {1399 chat.slice(firstId, messageId).forEach((message, id) => {
1400 messageElements.push(addOneMessage(message, { scroll: false, forceId: firstId + id, showSwipes: false, insert: false }));1400 messageElements.push(updateMessageElement(message, { messageId: firstId + id }));
1401 });1401 });
1402 // This could be faster: https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentElement1402 // This could be faster: https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentElement
1403 // Fallback to chatElement if the button isn't where it's expected to be.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 if (messages.length > 0) {1458 if (messages.length > 0) {
1459 const newMessageElements = messages.map((message, offset) => {1459 const newMessageElements = messages.map((message, offset) => {
1460 const i = startIndex + offset;1460 const i = startIndex + offset;
1461 const messageElement = addOneMessage(message, { scroll: false, forceId: i, showSwipes: false, insert: false });1461 const messageElement = updateMessageElement(message, { messageId: i });
14621462
1463 return messageElement[0];1463 return messageElement[0];
1464 });1464 });
@@ -2430,16 +2430,15 @@ function getMessageTextHTML(message, { messageId = chat.indexOf(message) }) {
2430 * Adds a single message to the chat.2430 * Adds a single message to the chat.
2431 * @param {ChatMessage} mes Message object2431 * @param {ChatMessage} mes Message object
2432 * @param {object} [options] Options2432 * @param {object} [options] Options
2433 * @param {string} [options.type='normal'] Message type2433 * @param {string} [options.type=undefined|'swipe'] Deprecated. Use updateMessageElement instead.
2434 * @param {number} [options.insertAfter=null] Message ID to insert the new message after2434 * @param {number} [options.insertAfter=null] Message ID to insert the new message after
2435 * @param {boolean} [options.scroll=true] Whether to scroll to the new message2435 * @param {boolean} [options.scroll=true] Whether to scroll to the new message
2436 * @param {number} [options.insertBefore=null] Message ID to insert the new message before2436 * @param {number} [options.insertBefore=null] Message ID to insert the new message before
2437 * @param {number} [options.forceId=null] Force the message ID2437 * @param {number} [options.forceId=null] Force the message ID
2438 * @param {boolean} [options.showSwipes=true] Whether to refresh the swipe buttons.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 * @returns {JQuery<HTMLElement>} The newly added message element2439 * @returns {JQuery<HTMLElement>} The newly added message element
2441 */2440 */
2442export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll = true, insertBefore = null, forceId = null, showSwipes = true, insert = true } = {}) {2441export function addOneMessage(mes, { type = undefined, insertAfter = null, scroll = true, insertBefore = null, forceId = null, showSwipes = true } = {}) {
2443 // Callers push the new message to chat before calling addOneMessage2442 // Callers push the new message to chat before calling addOneMessage
2444 const messageId = (() => {2443 const messageId = (() => {
2445 if (typeof forceId === 'number') {2444 if (typeof forceId === 'number') {
@@ -2458,6 +2457,55 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
2458 return chat.length - 1;2457 return chat.length - 1;
2459 })();2458 })();
24602459
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 */
2508export function updateMessageElement(mes, { messageId = chat.length - 1, messageElement = messageTemplate.clone(), adjustMediaScroll = SCROLL_BEHAVIOR.NONE } = {}) {
24612509
2462 let avatarImg = getThumbnailUrl('persona', user_avatar);2510 let avatarImg = getThumbnailUrl('persona', user_avatar);
24632511
@@ -2487,19 +2535,6 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
2487 const tokenCount = mes.extra?.token_count;2535 const tokenCount = mes.extra?.token_count;
2488 const { timerValue, timerTitle } = formatGenerationTimer(mes.gen_started, mes.gen_finished, mes.extra?.token_count, mes.extra?.reasoning_duration, mes.extra?.time_to_first_token);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);
24892537
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 messageElement.attr({2538 messageElement.attr({
2504 'mesid': messageId,2539 'mesid': messageId,
2505 'swipeid': mes.swipe_id ?? 0,2540 'swipeid': mes.swipe_id ?? 0,
@@ -2533,18 +2568,6 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
2533 insertSVGIcon(messageElement, mes.extra);2568 insertSVGIcon(messageElement, mes.extra);
2534 }2569 }
25352570
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 if (mes?.extra?.isSmallSys === true) {2571 if (mes?.extra?.isSmallSys === true) {
2549 messageElement.addClass('smallSysMes');2572 messageElement.addClass('smallSysMes');
2550 }2573 }
@@ -2560,7 +2583,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
2560 $(this).parent().html('<div class="missing-avatar fa-solid fa-user-slash"></div>');2583 $(this).parent().html('<div class="missing-avatar fa-solid fa-user-slash"></div>');
2561 });2584 });
25622585
2563 appendMediaToMessage(mes, messageElement, scroll ? SCROLL_BEHAVIOR.ADJUST : SCROLL_BEHAVIOR.NONE);2586 appendMediaToMessage(mes, messageElement, adjustMediaScroll);
2564 messageElement.find('.mes_text').html(messageHTML);2587 messageElement.find('.mes_text').html(messageHTML);
2565 addCopyToCodeBlocks(messageElement);2588 addCopyToCodeBlocks(messageElement);
25662589
@@ -2569,26 +2592,6 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
2569 updateSwipeCounter(messageId, { message: mes, messageElement });2592 updateSwipeCounter(messageId, { message: mes, messageElement });
2570 }2593 }
25712594
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 return messageElement;2595 return messageElement;
2593}2596}
25942597
@@ -11703,7 +11706,7 @@ jQuery(async function () {
11703 }11706 }
1170411707
11705 chat.splice(Number(this_edit_mes_id) + 1, 0, clone);11708 chat.splice(Number(this_edit_mes_id) + 1, 0, clone);
11706 const newMessageElement = addOneMessage(clone, { insert: false });11709 const newMessageElement = updateMessageElement(clone);
11707 this_edit_mes_element.after(newMessageElement);11710 this_edit_mes_element.after(newMessageElement);
1170811711
11709 updateViewMessageIds();11712 updateViewMessageIds();
public/scripts/slash-commands.js+3 -2
@@ -54,6 +54,7 @@ import {
54 system_avatar,54 system_avatar,
55 system_message_types,55 system_message_types,
56 this_chid,56 this_chid,
57 updateMessageElement,
57} from '../script.js';58} from '../script.js';
58import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';59import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
59import { SlashCommandParserError } from './slash-commands/SlashCommandParserError.js';60import { SlashCommandParserError } from './slash-commands/SlashCommandParserError.js';
@@ -4642,7 +4643,7 @@ async function messageRoleCallback(args, role) {
4642 await eventSource.emit(event_types.MESSAGE_EDITED, modifyAt);4643 await eventSource.emit(event_types.MESSAGE_EDITED, modifyAt);
4643 const existingMessage = chatElement.find(`.mes[mesid="${modifyAt}"]`);4644 const existingMessage = chatElement.find(`.mes[mesid="${modifyAt}"]`);
4644 if (existingMessage.length) {4645 if (existingMessage.length) {
4645 const newMessageElement = addOneMessage(message, { forceId: modifyAt, insert: false, scroll: false });4646 const newMessageElement = updateMessageElement(message, { messageId: modifyAt });
4646 existingMessage.after(newMessageElement);4647 existingMessage.after(newMessageElement);
4647 existingMessage.remove();4648 existingMessage.remove();
4648 }4649 }
@@ -4708,7 +4709,7 @@ async function messageNameCallback(args, name) {
4708 await eventSource.emit(event_types.MESSAGE_EDITED, modifyAt);4709 await eventSource.emit(event_types.MESSAGE_EDITED, modifyAt);
4709 const existingMessage = chatElement.find(`.mes[mesid="${modifyAt}"]`);4710 const existingMessage = chatElement.find(`.mes[mesid="${modifyAt}"]`);
4710 if (existingMessage.length) {4711 if (existingMessage.length) {
4711 const newMessageElement = addOneMessage(message, { forceId: modifyAt, insert: false, scroll: false });4712 const newMessageElement = updateMessageElement(message, { messageId: modifyAt });
4712 existingMessage.after(newMessageElement);4713 existingMessage.after(newMessageElement);
4713 existingMessage.remove();4714 existingMessage.remove();
4714 }4715 }