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 -54Showing whitespace changes
public/script.js+55 -52
@@ -1397,7 +1397,7 @@ export async function showMoreMessages(messagesToLoad = null) {
13971397 const firstId = clamp(messageId - count, 0, Infinity);
13981398 const messageElements = [];
13991399 chat.slice(firstId, messageId).forEach((message, id) => {
14001400 messageElements.push(addOneMessageupdateMessageElement(message, { scroll: false, forceIdmessageId: firstId + id, showSwipes: false, insert: false }));
14011401 });
14021402 // This could be faster: https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentElement
14031403 // 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 =
14581458 if (messages.length > 0) {
14591459 const newMessageElements = messages.map((message, offset) => {
14601460 const i = startIndex + offset;
14611461 const messageElement = addOneMessageupdateMessageElement(message, { scroll: false, forceIdmessageId: i, showSwipes: false, insert: false });
14621462
14631463 return messageElement[0];
14641464 });
@@ -2430,16 +2430,15 @@ function getMessageTextHTML(message, { messageId = chat.indexOf(message) }) {
24302430 * Adds a single message to the chat.
24312431 * @param {ChatMessage} mes Message object
24322432 * @param {object} [options] Options
24332433 * @param {string} [options.type=undefined|'normalswipe'] MessageDeprecated. typeUse updateMessageElement instead.
24342434 * @param {number} [options.insertAfter=null] Message ID to insert the new message after
24352435 * @param {boolean} [options.scroll=true] Whether to scroll to the new message
24362436 * @param {number} [options.insertBefore=null] Message ID to insert the new message before
24372437 * @param {number} [options.forceId=null] Force the message ID
24382438 * @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.
24402439 * @returns {JQuery<HTMLElement>} The newly added message element
24412440 */
24422441export function addOneMessage(mes, { type = 'normal'undefined, insertAfter = null, scroll = true, insertBefore = null, forceId = null, showSwipes = true, insert = true } = {}) {
24432442 // Callers push the new message to chat before calling addOneMessage
24442443 const messageId = (() => {
24452444 if (typeof forceId === 'number') {
@@ -2458,6 +2457,55 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
24582457 return chat.length - 1;
24592458 })();
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+ */
2508+export function updateMessageElement(mes, { messageId = chat.length - 1, messageElement = messageTemplate.clone(), adjustMediaScroll = SCROLL_BEHAVIOR.NONE } = {}) {
24612509
24622510 let avatarImg = getThumbnailUrl('persona', user_avatar);
24632511
@@ -2487,19 +2535,6 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
24872535 const tokenCount = mes.extra?.token_count;
24882536 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-
25032538 messageElement.attr({
25042539 'mesid': messageId,
25052540 'swipeid': mes.swipe_id ?? 0,
@@ -2533,18 +2568,6 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
25332568 insertSVGIcon(messageElement, mes.extra);
25342569 }
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-
25482571 if (mes?.extra?.isSmallSys === true) {
25492572 messageElement.addClass('smallSysMes');
25502573 }
@@ -2560,7 +2583,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
25602583 $(this).parent().html('<div class="missing-avatar fa-solid fa-user-slash"></div>');
25612584 });
25622585
25632586 appendMediaToMessage(mes, messageElement, scroll ? SCROLL_BEHAVIOR.ADJUST : SCROLL_BEHAVIOR.NONEadjustMediaScroll);
25642587 messageElement.find('.mes_text').html(messageHTML);
25652588 addCopyToCodeBlocks(messageElement);
25662589
@@ -2569,26 +2592,6 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
25692592 updateSwipeCounter(messageId, { message: mes, messageElement });
25702593 }
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-
25922595 return messageElement;
25932596}
25942597
@@ -11703,7 +11706,7 @@ jQuery(async function () {
1170311706 }
1170411707
1170511708 chat.splice(Number(this_edit_mes_id) + 1, 0, clone);
1170611709 const newMessageElement = addOneMessageupdateMessageElement(clone, { insert: false });
1170711710 this_edit_mes_element.after(newMessageElement);
1170811711
1170911712 updateViewMessageIds();
public/scripts/slash-commands.js+3 -2
@@ -54,6 +54,7 @@ import {
5454 system_avatar,
5555 system_message_types,
5656 this_chid,
57+ updateMessageElement,
5758} from '../script.js';
5859import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
5960import { SlashCommandParserError } from './slash-commands/SlashCommandParserError.js';
@@ -4642,7 +4643,7 @@ async function messageRoleCallback(args, role) {
46424643 await eventSource.emit(event_types.MESSAGE_EDITED, modifyAt);
46434644 const existingMessage = chatElement.find(`.mes[mesid="${modifyAt}"]`);
46444645 if (existingMessage.length) {
46454646 const newMessageElement = addOneMessageupdateMessageElement(message, { forceIdmessageId: modifyAt, insert: false, scroll: false });
46464647 existingMessage.after(newMessageElement);
46474648 existingMessage.remove();
46484649 }
@@ -4708,7 +4709,7 @@ async function messageNameCallback(args, name) {
47084709 await eventSource.emit(event_types.MESSAGE_EDITED, modifyAt);
47094710 const existingMessage = chatElement.find(`.mes[mesid="${modifyAt}"]`);
47104711 if (existingMessage.length) {
47114712 const newMessageElement = addOneMessageupdateMessageElement(message, { forceIdmessageId: modifyAt, insert: false, scroll: false });
47124713 existingMessage.after(newMessageElement);
47134714 existingMessage.remove();
47144715 }