Implement lazy loading for background menu (#4110) * debug: Enhance logging for IntersectionObserver and root element This commit adds more detailed console logging to `backgrounds.js` to help you diagnose issues with the lazy loading mechanism. Key changes: - Added logs to trace calls to `activateLazyLoader` from `getBackgrounds` and `getChatBackgroundsList`. - Added a log at the start of `activateLazyLoader`. - Added a check and log for the existence of the `#Backgrounds` element, which is intended as the `root` for the IntersectionObserver. - Logged the options being passed to the IntersectionObserver. - Ensured the IntersectionObserver can handle a null rootElement (which would default to the viewport). * feat: Implement lazy loading for background image thumbnails This commit introduces lazy loading for background image thumbnails to improve initial load performance and reduce unnecessary data transfer. Key changes: - Modified `getBackgroundFromTemplate` to store image URLs in a `data-bg-src` attribute instead of directly setting the `background-image` style. A `lazy-load-background` class is added to these elements. - Introduced a new `activateLazyLoader` function that uses the `IntersectionObserver` API. This function monitors elements with the `lazy-load-background` class and only loads their `background-image` (from `data-bg-src`) when they are about to become visible in the viewport. - Integrated `activateLazyLoader` into `getBackgrounds` and `getChatBackgroundsList` so that it's activated after background placeholder elements are created. - Added checks at the beginning of `getBackgrounds` and `getChatBackgroundsList` to prevent them from re-populating the thumbnail lists if they already contain elements, avoiding redundant processing. This ensures that: 1. No background thumbnails are downloaded when the application first loads. 2. When the background selection menu is opened, the list of thumbnails is generated as placeholders, but the actual images are not downloaded. 3. Thumbnail images are only downloaded from the server as they scroll into view. * I've made some updates to help diagnose why background images may not be loading. Specifically, I've: - Added logging to see when the lazy loader is activated and how many elements it's working with. - Included logs to track when the IntersectionObserver is triggered for an element and its status. - Added logging for the image URL being applied. - Included a warning if an image URL isn't found for an element that should be loading. Additionally, I've configured the IntersectionObserver to use the '#Backgrounds' element as the reference point for its calculations, with a 10% threshold. * Optimize background menu loading and add toggleable logging This PR addresses UI lag caused by the background selection menu. Changes: - Optimized Background Loading: - The background selection menu (`#Backgrounds`) now unloads non-selected and non-locked background images when it's closed. This is achieved by setting `style.backgroundImage = 'none'` for the respective elements. - When the menu is reopened, the background images are repopulated by calling the existing `getBackgrounds()` and `getChatBackgroundsList()` functions, which also re-initialize the lazy loader. - A `MutationObserver` is used to detect when the `#Backgrounds` menu drawer is opened or closed by observing changes to its `classList` (specifically the `closedDrawer` class). - Toggleable Debug Logging: - Added a `DEBUG_BACKGROUND_LOADING` constant at the top of `public/scripts/backgrounds.js`. - All `console.log`, `console.warn`, and `console.error` statements related to the background loading/unloading logic and the lazy loader are now conditional on this flag. Set `DEBUG_BACKGROUND_LOADING = true` to enable these logs for debugging purposes. Benefits: - Improves UI performance by reducing the number of images actively loaded in the DOM when the background menu is not in use. - Reduces main background playback lag that was exacerbated by having many backgrounds loaded. - Provides a way to enable detailed logging for troubleshooting background loading issues without cluttering the console during normal operation. * I can optimize the background menu loading. I'll detect when the background select menu is opened and closed. When the menu is closed, I will remove non-selected and non-locked backgrounds from the DOM to improve performance. When the menu is opened, I will repopulate the backgrounds. This will prevent UI lag caused by too many background images being loaded simultaneously. * lint syntax and spacing fix * add url check back in * Post-merge clean-up * Disconnecting previous observer on lazy load init --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>

e75da145636ac15c527d038e8a0605d62ecc35fa

L <123923688+Vibecoder9000@users.noreply.github.com>

Signed
1 files changed, +46 -7Showing whitespace changes
public/scripts/backgrounds.js+46 -7
@@ -13,6 +13,7 @@ const LIST_METADATA_KEY = 'chat_backgrounds';
13// A single transparent PNG pixel used as a placeholder for errored backgrounds13// A single transparent PNG pixel used as a placeholder for errored backgrounds
14const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';14const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
15const PNG_PIXEL_BLOB = new Blob([Uint8Array.from(atob(PNG_PIXEL), c => c.charCodeAt(0))], { type: 'image/png' });15const PNG_PIXEL_BLOB = new Blob([Uint8Array.from(atob(PNG_PIXEL), c => c.charCodeAt(0))], { type: 'image/png' });
16const PLACEHOLDER_IMAGE = `url('data:image/png;base64,${PNG_PIXEL}')`;
1617
17/**18/**
18 * Storage for frontend-generated background thumbnails.19 * Storage for frontend-generated background thumbnails.
@@ -26,6 +27,12 @@ const THUMBNAIL_STORAGE = localforage.createInstance({ name: 'SillyTavern_Thumbn
26 */27 */
27const THUMBNAIL_BLOBS = new Map();28const THUMBNAIL_BLOBS = new Map();
2829
30/**
31 * Global IntersectionObserver instance for lazy loading backgrounds
32 * @type {IntersectionObserver|null}
33 */
34let lazyLoadObserver = null;
35
29export let background_settings = {36export let background_settings = {
30 name: '__transparent.png',37 name: '__transparent.png',
31 url: generateUrlParameter('__transparent.png', false),38 url: generateUrlParameter('__transparent.png', false),
@@ -95,6 +102,7 @@ async function getChatBackgroundsList() {
95 const template = await getBackgroundFromTemplate(bg, true);102 const template = await getBackgroundFromTemplate(bg, true);
96 $('#bg_custom_content').append(template);103 $('#bg_custom_content').append(template);
97 }104 }
105 activateLazyLoader();
98}106}
99107
100function getBackgroundPath(fileUrl) {108function getBackgroundPath(fileUrl) {
@@ -434,22 +442,52 @@ export async function getBackgrounds() {
434 const response = await fetch('/api/backgrounds/all', {442 const response = await fetch('/api/backgrounds/all', {
435 method: 'POST',443 method: 'POST',
436 headers: getRequestHeaders(),444 headers: getRequestHeaders(),
437 body: JSON.stringify({445 body: JSON.stringify({}),
438 '': '',
439 }),
440 });446 });
441 if (response.ok) {447 if (response.ok) {
442 const getData = await response.json();448 const getData = await response.json();
443 //background = getData;
444 //console.log(getData.length);
445 $('#bg_menu_content').children('div').remove();449 $('#bg_menu_content').children('div').remove();
446 for (const bg of getData) {450 for (const bg of getData) {
447 const template = await getBackgroundFromTemplate(bg, false);451 const template = await getBackgroundFromTemplate(bg, false);
448 $('#bg_menu_content').append(template);452 $('#bg_menu_content').append(template);
449 }453 }
454 activateLazyLoader();
450 }455 }
451}456}
452457
458function activateLazyLoader() {
459 // Disconnect previous observer to prevent memory leaks
460 if (lazyLoadObserver) {
461 lazyLoadObserver.disconnect();
462 lazyLoadObserver = null;
463 }
464
465 const lazyLoadElements = document.querySelectorAll('.lazy-load-background');
466
467 const options = {
468 root: null,
469 rootMargin: '200px',
470 threshold: 0.01,
471 };
472
473 lazyLoadObserver = new IntersectionObserver((entries, observer) => {
474 entries.forEach(entry => {
475 if (entry.target instanceof HTMLElement && entry.isIntersecting) {
476 const imageUrl = entry.target.dataset.bgSrc;
477 if (imageUrl) {
478 entry.target.style.backgroundImage = `url('${imageUrl}')`;
479 }
480 entry.target.classList.remove('lazy-load-background');
481 observer.unobserve(entry.target);
482 }
483 });
484 }, options);
485
486 lazyLoadElements.forEach(element => {
487 lazyLoadObserver.observe(element);
488 });
489}
490
453/**491/**
454 * Gets the CSS URL of the background492 * Gets the CSS URL of the background
455 * @param {Element} block493 * @param {Element} block
@@ -481,13 +519,14 @@ async function getBackgroundFromTemplate(bg, isCustom) {
481 : isCustom519 : isCustom
482 ? bg520 ? bg
483 : getThumbnailUrl('bg', bg);521 : getThumbnailUrl('bg', bg);
484 const thumbnailCssUrl = `url('${thumbnailUrl}')`;
485522
486 template.attr('title', title);523 template.attr('title', title);
487 template.attr('bgfile', bg);524 template.attr('bgfile', bg);
488 template.attr('custom', String(isCustom));525 template.attr('custom', String(isCustom));
489 template.data('url', url);526 template.data('url', url);
490 template.css('background-image', thumbnailCssUrl);527 template.attr('data-bg-src', thumbnailUrl);
528 template.addClass('lazy-load-background');
529 template.css('background-image', PLACEHOLDER_IMAGE);
491 template.find('.BGSampleTitle').text(friendlyTitle);530 template.find('.BGSampleTitle').text(friendlyTitle);
492 return template;531 return template;
493}532}