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>
Signed| @@ -13,6 +13,7 @@ const LIST_METADATA_KEY = 'chat_backgrounds'; | ||
| 13 | 13 | // A single transparent PNG pixel used as a placeholder for errored backgrounds |
| 14 | 14 | const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='; |
| 15 | 15 | const PNG_PIXEL_BLOB = new Blob([Uint8Array.from(atob(PNG_PIXEL), c => c.charCodeAt(0))], { type: 'image/png' }); |
| 16 | +const PLACEHOLDER_IMAGE = `url('data:image/png;base64,${PNG_PIXEL}')`; | |
| 16 | 17 | |
| 17 | 18 | /** |
| 18 | 19 | * Storage for frontend-generated background thumbnails. |
| @@ -26,6 +27,12 @@ const THUMBNAIL_STORAGE = localforage.createInstance({ name: 'SillyTavern_Thumbn | ||
| 26 | 27 | */ |
| 27 | 28 | const THUMBNAIL_BLOBS = new Map(); |
| 28 | 29 | |
| 30 | +/** | |
| 31 | + * Global IntersectionObserver instance for lazy loading backgrounds | |
| 32 | + * @type {IntersectionObserver|null} | |
| 33 | + */ | |
| 34 | +let lazyLoadObserver = null; | |
| 35 | + | |
| 29 | 36 | export let background_settings = { |
| 30 | 37 | name: '__transparent.png', |
| 31 | 38 | url: generateUrlParameter('__transparent.png', false), |
| @@ -95,6 +102,7 @@ async function getChatBackgroundsList() { | ||
| 95 | 102 | const template = await getBackgroundFromTemplate(bg, true); |
| 96 | 103 | $('#bg_custom_content').append(template); |
| 97 | 104 | } |
| 105 | + activateLazyLoader(); | |
| 98 | 106 | } |
| 99 | 107 | |
| 100 | 108 | function getBackgroundPath(fileUrl) { |
| @@ -434,20 +442,50 @@ export async function getBackgrounds() { | ||
| 434 | 442 | const response = await fetch('/api/backgrounds/all', { |
| 435 | 443 | method: 'POST', |
| 436 | 444 | headers: getRequestHeaders(), |
| 437 | 445 | body: JSON.stringify({}), |
| 438 | - '': '', | |
| 439 | - }), | |
| 440 | 446 | }); |
| 441 | 447 | if (response.ok) { |
| 442 | 448 | const getData = await response.json(); |
| 443 | - //background = getData; | |
| 444 | - //console.log(getData.length); | |
| 445 | 449 | $('#bg_menu_content').children('div').remove(); |
| 446 | 450 | for (const bg of getData) { |
| 447 | 451 | const template = await getBackgroundFromTemplate(bg, false); |
| 448 | 452 | $('#bg_menu_content').append(template); |
| 449 | 453 | } |
| 454 | + activateLazyLoader(); | |
| 455 | + } | |
| 456 | +} | |
| 457 | + | |
| 458 | +function activateLazyLoader() { | |
| 459 | + // Disconnect previous observer to prevent memory leaks | |
| 460 | + if (lazyLoadObserver) { | |
| 461 | + lazyLoadObserver.disconnect(); | |
| 462 | + lazyLoadObserver = null; | |
| 450 | 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 | + }); | |
| 451 | 489 | } |
| 452 | 490 | |
| 453 | 491 | /** |
| @@ -481,13 +519,14 @@ async function getBackgroundFromTemplate(bg, isCustom) { | ||
| 481 | 519 | : isCustom |
| 482 | 520 | ? bg |
| 483 | 521 | : getThumbnailUrl('bg', bg); |
| 484 | - const thumbnailCssUrl = `url('${thumbnailUrl}')`; | |
| 485 | 522 | |
| 486 | 523 | template.attr('title', title); |
| 487 | 524 | template.attr('bgfile', bg); |
| 488 | 525 | template.attr('custom', String(isCustom)); |
| 489 | 526 | template.data('url', url); |
| 490 | 527 | template.cssattr('backgrounddata-imagebg-src', thumbnailCssUrlthumbnailUrl); |
| 528 | + template.addClass('lazy-load-background'); | |
| 529 | + template.css('background-image', PLACEHOLDER_IMAGE); | |
| 491 | 530 | template.find('.BGSampleTitle').text(friendlyTitle); |
| 492 | 531 | return template; |
| 493 | 532 | } |