[PR #3726] [MERGED] LazyBookshelf optimizations #4065

Closed
opened 2026-04-25 00:18:11 +02:00 by adam · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/advplyr/audiobookshelf/pull/3726
Author: @mikiher
Created: 12/16/2024
Status: Merged
Merged: 12/26/2024
Merged by: @advplyr

Base: masterHead: lazy-bookshelf-optimizations


📝 Commits (5)

  • ba55413 LazyBookshelf optimizations
  • 9218804 Introduce static skeleton cards
  • 004210e reuse entityTransform in mountEntityCard
  • 780c0dc Merge branch 'master' into lazy-bookshelf-optimizations
  • 4d8501c Update skeleton card to have box shadow, fix last row of skeleton cards

📊 Changes

4 files changed (+95 additions, -80 deletions)

View changed files

📝 client/components/app/LazyBookshelf.vue (+87 -73)
📝 client/components/cards/AuthorCard.vue (+3 -0)
📝 client/components/cards/LazyBookCard.vue (+1 -1)
📝 client/mixins/bookshelfCardsHelpers.js (+4 -6)

📄 Description

Brief summary

The follwoing changes were made to optimize rendering of LazyBookshelf, especially during scrolling.

  1. Debouncing when scroll rate is high
  2. Wait for entities to be fetched before mounting them
  3. Cleaning up only after scrolling
  4. Calling rebuild directly instead of executeRebuild in settingsUpdated
  5. Adding passive: true option to scroll event listener
  6. Some additional refacroting

Which issue is fixed?

There's no existing issue.

In-depth Description

1. Debouncing when scroll rate is high

When scrolling using the mouse wheel (and, to a lesser extent, by dragging the scrollbar), scrolling events tend to fire very rapidly (up to ~60 times per second), and sometimes for a long while (like when setting the wheel to spin freely on a Logi Mx Master 3 like mine). This creates many redundant handleScroll calls (which, in turn, redundantly fetches and mounts many entities which are outside the visible display).

To counter this, I calculate the scrolling rate on each scroll event, and debounce the handleScroll call for 25 ms if the scroll rate is higher than 5 pixels/ms. This saves a lot of redundant fetching and rendering, and usually during the end of scrolling the rate goes below 5, so the debounding happens mostly in the middle of the scroll period, but not in the end.

Note that a side effect of this is that the bookshelf will go blank during a very fast and long scroll. I think this is acceptable, since you don't gain anything from seeing fast flying entities (the scroll bar is still moving, though, giving you an indication that scrolling happens)

2. Wait for entities to be fetched before mounting them

Up until now, handleScroll called loadPage but didn't wait for it to finish fetching the entities, and mounted empty entities, which caused the book placeholder flicker you sometimes saw. Now we only mount after all the relevant entities have been fetched.

3. Cleaning up only after scrolling

Up until now, every handleScroll call did a cleanup of non-visible entities, with this code:

      this.entityIndexesMounted = this.entityIndexesMounted.filter((_index) => {
        if (_index < firstBookIndex || _index >= lastBookIndex) {
          var el = document.getElementById(`book-card-${_index}`)
          if (el) el.remove()
          return false
        }
        return true
      })

I suspect this cleanup was slowing rendering because of the frequest dom changes. Plus, it only handled book entities, and not other types of entities (which have different ids). I added a debounce of 500 ms here, and fixed the bug.

4. Calling rebuild directly instead of executeRebuild in settingsUpdated

Doing this got rid of 250 ms delay before rebuild, which caused an annoying bookshelf flicker when changing the cover size.

5. Adding passive: true option to scroll event listener

This is supposed to enable smoother scrolling and improve scrolling performance when the listener doesn't need to call preventDefault.

6. Some additional refacroting

While doing all of this, I made a number of refactoring changes:

  • rewrote rebuild, and got rid of remountEntities
  • pagesLoaded now contains fetchEntities promises instead of booleans
  • fetchEntities is now always called only from loadPage

Things yet to be fixed

While testing this, I found that the server has very different response times for page fetches for different sortBy values. When sorting by Added at or Title, server response time is usually below 100 ms for a page fetch request. However when sorting by Size, for example, the response time tends to go up significantly, and when scrolling fast, the server is handling a number of requests simultaneously, and then it behaves really bad (all simultaneous requests take 5 or more seconds) - we've already seen that Sequelize/SQlite handles concurrent requests very poorly, and I suspect that this is the issue here as well.

I suspect that the poor performance on certain sortBy values is due to lack of proper indices, but will need to investigate further.

How have you tested this?

  • Lots of scrolling, using different scrolling devices...
  • Moving between cover sizes
  • Moving between different sortBy and filterBy values
  • Tested on all pages using LazyBookshelf (Library, Series, Collections, Playlists, Authors).
  • Tested on library with many books, series, and authors (~4k books)

🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/advplyr/audiobookshelf/pull/3726 **Author:** [@mikiher](https://github.com/mikiher) **Created:** 12/16/2024 **Status:** ✅ Merged **Merged:** 12/26/2024 **Merged by:** [@advplyr](https://github.com/advplyr) **Base:** `master` ← **Head:** `lazy-bookshelf-optimizations` --- ### 📝 Commits (5) - [`ba55413`](https://github.com/advplyr/audiobookshelf/commit/ba55413e63230311ba8c66cae93a6daa91dec1f9) LazyBookshelf optimizations - [`9218804`](https://github.com/advplyr/audiobookshelf/commit/921880445a197846d358840d26e4307eaa33b00d) Introduce static skeleton cards - [`004210e`](https://github.com/advplyr/audiobookshelf/commit/004210ee02ec9e518450e824101aa8c782797d7b) reuse entityTransform in mountEntityCard - [`780c0dc`](https://github.com/advplyr/audiobookshelf/commit/780c0dcb9978b990d174cc1ca9bfbf3a06c5b2f3) Merge branch 'master' into lazy-bookshelf-optimizations - [`4d8501c`](https://github.com/advplyr/audiobookshelf/commit/4d8501c34797ac389f10612024cc78ad4d63950a) Update skeleton card to have box shadow, fix last row of skeleton cards ### 📊 Changes **4 files changed** (+95 additions, -80 deletions) <details> <summary>View changed files</summary> 📝 `client/components/app/LazyBookshelf.vue` (+87 -73) 📝 `client/components/cards/AuthorCard.vue` (+3 -0) 📝 `client/components/cards/LazyBookCard.vue` (+1 -1) 📝 `client/mixins/bookshelfCardsHelpers.js` (+4 -6) </details> ### 📄 Description ## Brief summary The follwoing changes were made to optimize rendering of LazyBookshelf, especially during scrolling. 1. Debouncing when scroll rate is high 2. Wait for entities to be fetched before mounting them 3. Cleaning up only after scrolling 4. Calling `rebuild` directly instead of `executeRebuild` in `settingsUpdated` 5. Adding `passive: true` option to scroll event listener 6. Some additional refacroting ## Which issue is fixed? There's no existing issue. ## In-depth Description ### 1. Debouncing when scroll rate is high When scrolling using the mouse wheel (and, to a lesser extent, by dragging the scrollbar), scrolling events tend to fire very rapidly (up to ~60 times per second), and sometimes for a long while (like when setting the wheel to spin freely on a Logi Mx Master 3 like mine). This creates many redundant `handleScroll` calls (which, in turn, redundantly fetches and mounts many entities which are outside the visible display). To counter this, I calculate the scrolling rate on each scroll event, and debounce the `handleScroll` call for 25 ms if the scroll rate is higher than 5 pixels/ms. This saves a lot of redundant fetching and rendering, and usually during the end of scrolling the rate goes below 5, so the debounding happens mostly in the middle of the scroll period, but not in the end. Note that a side effect of this is that the bookshelf will go blank during a very fast and long scroll. I think this is acceptable, since you don't gain anything from seeing fast flying entities (the scroll bar is still moving, though, giving you an indication that scrolling happens) ### 2. Wait for entities to be fetched before mounting them Up until now, `handleScroll` called `loadPage` but didn't wait for it to finish fetching the entities, and mounted empty entities, **which caused the book placeholder flicker you sometimes saw**. Now we only mount after all the relevant entities have been fetched. ### 3. Cleaning up only after scrolling Up until now, every handleScroll call did a cleanup of non-visible entities, with this code: ```js this.entityIndexesMounted = this.entityIndexesMounted.filter((_index) => { if (_index < firstBookIndex || _index >= lastBookIndex) { var el = document.getElementById(`book-card-${_index}`) if (el) el.remove() return false } return true }) ``` I suspect this cleanup was slowing rendering because of the frequest dom changes. Plus, it only handled book entities, and not other types of entities (which have different ids). I added a debounce of 500 ms here, and fixed the bug. ### 4. Calling `rebuild` directly instead of `executeRebuild` in `settingsUpdated` Doing this got rid of 250 ms delay before rebuild, which caused an annoying bookshelf flicker when changing the cover size. ### 5. Adding `passive: true` option to scroll event listener This is supposed to enable smoother scrolling and improve scrolling performance when the listener doesn't need to call `preventDefault`. ### 6. Some additional refacroting While doing all of this, I made a number of refactoring changes: - rewrote `rebuild`, and got rid of `remountEntities` - `pagesLoaded` now contains `fetchEntities` promises instead of booleans - `fetchEntities` is now always called only from `loadPage` ### Things yet to be fixed While testing this, I found that the server has very different response times for page fetches for different `sortBy` values. When sorting by `Added at` or `Title`, server response time is usually below 100 ms for a page fetch request. However when sorting by `Size`, for example, the response time tends to go up significantly, and when scrolling fast, the server is handling a number of requests simultaneously, and then it behaves really bad (all simultaneous requests take 5 or more _seconds_) - we've already seen that Sequelize/SQlite handles concurrent requests very poorly, and I suspect that this is the issue here as well. I suspect that the poor performance on certain sortBy values is due to lack of proper indices, but will need to investigate further. ## How have you tested this? - Lots of scrolling, using different scrolling devices... - Moving between cover sizes - Moving between different `sortBy` and `filterBy` values - Tested on all pages using LazyBookshelf (Library, Series, Collections, Playlists, Authors). - Tested on library with many books, series, and authors (~4k books) --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
adam added the pull-request label 2026-04-25 00:18:11 +02:00
adam closed this issue 2026-04-25 00:18:11 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/audiobookshelf#4065