UX: Implementation/improvement ideas for infinite scrolling #1408

Closed
opened 2026-02-20 00:12:05 -05:00 by deekerman · 15 comments
Owner

Originally created by @heikomat on GitHub (Apr 16, 2022).

Is your feature request related to a problem? Please describe.

When scrolling through multiple month or years worth of pictures, the application gets slower and slower over time while consuming ever increasing amounts of ram, until it gets unusably slow (opening pictures takes >5 seconds)

I'm pretty sure that the culprit is the missing virtualization in the picture grid. The browser has to manage tens of thousands of picture nodes, likely adding up to hundrets of thousands of dom nodes, even though only a couple dozen are visible at a time

Describe alternatives you've considered

A workaround is to filter the pictures by month. Looking at the picutres of a single month, then scrolling back to the top, change the month and look at the next pictures.

Additional context

this comment already talks about virtualization, but the corresponding issue was ultimately closed, as it was about timeline-view, and virtualization might have only been a part of that discussion.


Suggestion for implementation

I know that the relative positioning that is currently being used makes the UI easy to implement, but is not really compatible with conventional virtualization.

Not long ago i faced a similar problem where non-absolute-positioned elements had to be virtualized.
The solution was to use the IntersectionObserver so that every element could just get informed by the browser if it is visible or not, instead of implementing some logic to calculate that.
If an element is not visible, it just renders a single div of the same dimensions as the original as placeholder.
Not quite real virtualization, as it still requires the placeholders, but should be much, much faster when scrolling through large lists

Using an IntersectionObserver

  • is easy to implement (because no virtualization-logic is required)
  • is fast (because the browser has that info anyway)
  • is supported by all major browsers
  • works with relative positioning and deeply nested and/or complex structures
  • natively supports things like rootMargin that can be used to render a couple more lines than actually visible (reduces flickering while scrolling)

Some hints

  • As is demonstrated here, one can use a single IntersectionObserver to observe as many elements as required, increasing performance compared to using multiple intersection-observers
  • Because of the endless-scrolling nature of the picture grid, one can assume that on initial mount, the picture is visible. That should prevent flickering

I would be interested in trying to implement this myself, but i'm not sure if i could find the time to do so

Originally created by @heikomat on GitHub (Apr 16, 2022). **Is your feature request related to a problem? Please describe.** When scrolling through multiple month or years worth of pictures, the application gets slower and slower over time while consuming ever increasing amounts of ram, until it gets unusably slow (opening pictures takes >5 seconds) I'm pretty sure that the culprit is the missing virtualization in the picture grid. The browser has to manage tens of thousands of picture nodes, likely adding up to hundrets of thousands of dom nodes, even though only a couple dozen are visible at a time **Describe alternatives you've considered** A workaround is to filter the pictures by month. Looking at the picutres of a single month, then scrolling back to the top, change the month and look at the next pictures. **Additional context** [this comment](https://github.com/photoprism/photoprism/issues/500#issuecomment-727917142) already talks about virtualization, but the corresponding issue was ultimately closed, as it was about timeline-view, and virtualization might have only been a part of that discussion. --- **Suggestion for implementation** I know that the relative positioning that is currently being used makes the UI easy to implement, but is not really compatible with conventional virtualization. Not long ago i faced a similar problem where non-absolute-positioned elements had to be virtualized. The solution was to use the [IntersectionObserver](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) so that every element could just get informed by the browser if it is visible or not, instead of implementing some logic to calculate that. If an element is not visible, it just renders a single div of the same dimensions as the original as placeholder. Not quite real virtualization, as it still requires the placeholders, but should be much, much faster when scrolling through large lists Using an IntersectionObserver - is easy to implement (because no virtualization-logic is required) - is fast (because the browser has that info anyway) - is [supported by all major browsers](https://caniuse.com/?search=IntersectionObserver) - works with relative positioning and deeply nested and/or complex structures - natively supports things like [rootMargin](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/rootMargin) that can be used to render a couple more lines than actually visible (reduces flickering while scrolling) **Some hints** - As is demonstrated [here](https://www.bennadel.com/blog/3954-intersectionobserver-api-performance-many-vs-shared-in-angular-11-0-5.htm), one can use a single IntersectionObserver to observe as many elements as required, increasing performance compared to using multiple intersection-observers - Because of the endless-scrolling nature of the picture grid, one can assume that on initial mount, the picture is visible. That should prevent flickering I would be interested in trying to implement this myself, but i'm not sure if i could find the time to do so
deekerman 2026-02-20 00:12:05 -05:00
Author
Owner

@lastzero commented on GitHub (Apr 16, 2022):

You are right and we are aware of this, see #152. In one of the many in-between steps, we have improved query performance in the backend and eliminated duplicate fetching of the same data for the viewer (which already saves memory and makes the UI more responsive, although you still can't scroll infinitely):

Your suggestions may be helpful later, but are perhaps better off as implementation notes/ideas in our developer guide? The UI section is still pretty empty, as we - like most developers - don't have enough time to do everything we want and find useful:

Feel free to create an "Infinite Scrolling" page and send a pull request to https://github.com/photoprism/photoprism-docs!

@lastzero commented on GitHub (Apr 16, 2022): You are right and we are aware of this, see #152. In one of the many in-between steps, we have improved query performance in the backend and eliminated duplicate fetching of the same data for the viewer (which already saves memory and makes the UI more responsive, although you still can't scroll infinitely): - #1438 Your suggestions may be helpful later, but are perhaps better off as implementation notes/ideas in our developer guide? The UI section is still pretty empty, as we - like most developers - don't have enough time to do everything we want and find useful: - https://docs.photoprism.app/developer-guide/ui/introduction/ Feel free to create an "Infinite Scrolling" page and send a pull request to https://github.com/photoprism/photoprism-docs!
Author
Owner

@heikomat commented on GitHub (Apr 16, 2022):

As it turns out, i do have a couple hours to spend today.
I'll try to setup a local dev environment and implement the feature (thank you for using docker, makes things so much easier!)
If it works good i'll open a PR here and add my virutalization hints to the docs.
If it doesn't work good i'll also add the findings to the docs.

@heikomat commented on GitHub (Apr 16, 2022): As it turns out, i do have a couple hours to spend today. I'll try to setup a local dev environment and implement the feature (thank you for using docker, makes things so much easier!) If it works good i'll open a PR here and add my virutalization hints to the docs. If it doesn't work good i'll also add the findings to the docs.
Author
Owner

@heikomat commented on GitHub (Apr 18, 2022):

@lastzero i've successfully virtualized the mosaic-view in about 70 lines of simple code and without any new dependencies:
github.com/photoprism/photoprism@ca3b0d233a

When scrolling to the end of a 3600 pictures list:

  • The scrolling-performance stayed consistent, compared to getting laggier and laggier
  • The memory-usage of the browser-tab at the end was at ~640MB vs 2.08GB without virtualization

The only downside: if a cards initial position is within the visible area, it now needs to render twice before the picture is can be seen (Once to determine if it is visible, and a second time after learning that it's visible). This makes the inital open just a touch slower.
We could easily make newly rendered cards act as if they are visible, but that would have its own downsides (new but non-visible cards would render twice initially, making the corresponding picture load unnecessarily etc.)

This is not yet PR-Ready because even though the changes are minimal, i have not yet run the tests, and i'd ideally virtualize the other photo-views as well.

If possible i'd appreciate some early feedback as i have zero vue experience (I'm a React + TypeScript guy)

@heikomat commented on GitHub (Apr 18, 2022): @lastzero i've successfully virtualized the mosaic-view in about 70 lines of simple code and without any new dependencies: https://github.com/photoprism/photoprism/commit/ca3b0d233a62e46f74d255acde87cbb74f2fa416 When scrolling to the end of a 3600 pictures list: - The scrolling-performance stayed consistent, compared to getting laggier and laggier - The memory-usage of the browser-tab at the end was at ~640MB vs 2.08GB without virtualization The only downside: if a cards initial position is within the visible area, it now needs to render twice before the picture is can be seen (Once to determine if it is visible, and a second time after learning that it's visible). This makes the inital open just a touch slower. We could easily make newly rendered cards act as if they are visible, but that would have its own downsides (new but non-visible cards would render twice initially, making the corresponding picture load unnecessarily etc.) This is not yet PR-Ready because even though the changes are minimal, i have not yet run the tests, and i'd ideally virtualize the other photo-views as well. If possible i'd appreciate some early feedback as i have zero vue experience (I'm a React + TypeScript guy)
Author
Owner

@lastzero commented on GitHub (Apr 20, 2022):

Sounds about right. I'm currently out of office and can't review any code on my phone. My understanding is that the layout size computation can be simplified with a few assumptions and a different API. Building infinite scrolling on top of what we have right now is undoubtedly more complicated and might require two passes.

@lastzero commented on GitHub (Apr 20, 2022): Sounds about right. I'm currently out of office and can't review any code on my phone. My understanding is that the layout size computation can be simplified with a few assumptions and a different API. Building infinite scrolling on top of what we have right now is undoubtedly more complicated and might require two passes.
Author
Owner

@heikomat commented on GitHub (Apr 24, 2022):

Just FIY, i continue working on this whenever i've got time (here's the branch).

Finding out what elements to render seems to be plenty fast.
What is not that fast is updating the component-state.
When updating for example the first/last visible index of the mosaic component or list component, vue seems to require a lot of cpu-time to figure out what dom-nodes to update (or to update them, even if they didn't change?).

Next step for me would therefore be to figure out how to accelerate this process.

In react-world i would now move the inner elements into own, memoized components. these would then only rerender if something changed. Need to find out if vue supports something similar

EDIT:
rendering a div instead of an empty v-card fixed the "vue takes long to update"-issue for the mosaic view :)

EDIT2: got the list-view to render fast aswell. cards are next

@heikomat commented on GitHub (Apr 24, 2022): Just FIY, i continue working on this whenever i've got time ([here's the branch](https://github.com/heikomat/photoprism/tree/feature/photo-view-virtualization)). Finding out what elements to render seems to be plenty fast. What is not that fast is updating the component-state. When updating for example the first/last visible index of the mosaic component or list component, vue seems to require a lot of cpu-time to figure out what dom-nodes to update (or to update them, even if they didn't change?). Next step for me would therefore be to figure out how to accelerate this process. In react-world i would now move the inner elements into own, memoized components. these would then only rerender if something changed. Need to find out if vue supports something similar EDIT: rendering a div instead of an empty v-card fixed the "vue takes long to update"-issue for the mosaic view :) EDIT2: got the list-view to render fast aswell. cards are next
Author
Owner

@lastzero commented on GitHub (Apr 24, 2022):

That is also my experience, but there might be ways to improve Vue performance by reducing the number of variables / values to observe in this.data. Ultimately, I believe it's best to reimplement performance critical views with vanilla JS and/or functional Vue components.

@lastzero commented on GitHub (Apr 24, 2022): That is also my experience, but there might be ways to improve Vue performance by reducing the number of variables / values to observe in `this.data`. Ultimately, I believe it's best to reimplement performance critical views with vanilla JS and/or functional Vue components.
Author
Owner

@heikomat commented on GitHub (Apr 24, 2022):

That is also my experience, but there might be ways to improve Vue performance by reducing the number of variables / values to observe in this.data. Ultimately, I believe it's best to reimplement performance critical views with vanilla JS and/or functional Vue components.

The performance is fine as long as the placeholder elements (the ones out of view) consist of only non-vue-elements. These elements (like regular html divs, buttons, spans etc.) Don't appear in the Vue dev tools and somehow don't cause high CPU when updating the components state!

In case of the mosaic view, I replaced the v-card placeholder with a div.

In case of the list view I replaced the v-img and the two v-btn with regular divs.

The vue-devtools then only show the elements that are actually in view, and virtualization works just as intended :)

So, impressive performance gains in the endless scrolling scenarios don't necessarily require much rework as long as some rules are being followed.

@heikomat commented on GitHub (Apr 24, 2022): > That is also my experience, but there might be ways to improve Vue performance by reducing the number of variables / values to observe in `this.data`. Ultimately, I believe it's best to reimplement performance critical views with vanilla JS and/or functional Vue components. The performance is fine as long as the placeholder elements (the ones out of view) consist of only non-vue-elements. These elements (like regular html divs, buttons, spans etc.) Don't appear in the Vue dev tools and somehow **don't cause high CPU when updating the components state!** In case of the mosaic view, I replaced the v-card placeholder with a div. In case of the list view I replaced the v-img and the two v-btn with regular divs. The vue-devtools then only show the elements that are actually in view, and virtualization works just as intended :) So, impressive performance gains in the endless scrolling scenarios don't *necessarily* require much rework as long as some rules are being followed.
Author
Owner

@srett commented on GitHub (Apr 27, 2022):

Hey, this is pretty cool and you seem to be making rather good progress. I'm usually working on the opposite end of the tech-stack so all this web stuff is just black magic to me. Great to see there can still be made some significant performance improvements with the current implementation.

I tried giving my kids an old tablet with just 1.5GB of RAM to browse the photos, but after scrolling a short while Chrome becomes completely unresponsive. Sounds like this could make it usable. :-) Big thumbs up!

@srett commented on GitHub (Apr 27, 2022): Hey, this is pretty cool and you seem to be making rather good progress. I'm usually working on the opposite end of the tech-stack so all this web stuff is just black magic to me. Great to see there can still be made some significant performance improvements with the current implementation. I tried giving my kids an old tablet with just 1.5GB of RAM to browse the photos, but after scrolling a short while Chrome becomes completely unresponsive. Sounds like this could make it usable. :-) Big thumbs up!
Author
Owner

@heikomat commented on GitHub (Apr 27, 2022):

Update: i found another Hour today. Cards are "virtualized" aswell.
Scrolling through 3600 pictures was way smoother, and the final ram usage went down from 2.6GB to 850MB.

Next steps: Testing other pages than "search", cleaning up the changes (deduplicate logic) and running the tests

@heikomat commented on GitHub (Apr 27, 2022): Update: i found another Hour today. Cards are "virtualized" aswell. Scrolling through 3600 pictures was way smoother, and the final ram usage went down from 2.6GB to 850MB. Next steps: Testing other pages than "search", cleaning up the changes (deduplicate logic) and running the tests
Author
Owner

@lastzero commented on GitHub (Apr 27, 2022):

Awesome, hope I have some time for testing (maybe even development) soon! Still busy with handling support, setting up our new company (see Twitter for the announcement), and updating our legal docs like privacy policy, terms of service, code of conduct, security policy, GDPR Statement,... Can't believe you need so many documents to get started 😂

@lastzero commented on GitHub (Apr 27, 2022): Awesome, hope I have some time for testing (maybe even development) soon! Still busy with handling support, setting up our new company (see Twitter for the announcement), and updating our legal docs like privacy policy, terms of service, code of conduct, security policy, GDPR Statement,... Can't believe you need so many documents to get started 😂
Author
Owner

@heikomat commented on GitHub (Apr 29, 2022):

@lastzero i finished up my changes , ran the automated tests and opened a PR: https://github.com/photoprism/photoprism/pull/2292

I haven't written any tests, though i'm not sure there is much i can write tests for, as these changes aren't supposed to add or alter any visual or functional behaviour.

Documentation regarding rules for this pseudo-virtualization have also not been added yet, but i'll do that when i find some more time

@heikomat commented on GitHub (Apr 29, 2022): @lastzero i finished up my changes , ran the automated tests and opened a PR: https://github.com/photoprism/photoprism/pull/2292 I haven't written any tests, though i'm not sure there is much i can write tests for, as these changes aren't supposed to add or alter any visual or functional behaviour. Documentation regarding rules for this pseudo-virtualization have also not been added yet, but i'll do that when i find some more time
Author
Owner

@staab commented on GitHub (Jun 9, 2022):

Just wanted to register my support for this — on my Pixel 3 the UI is extremely slow even after only a few pages of scrolling. I was just looking into this, but it looks like @heikomat has already made a ton of progress! One thing I might add after a brief look through this file is that line 552 might be the source of the high CPU usage: this.$forceUpdate(); — simply removing that might significantly reduce the need for virtualization (though that certainly wouldn't hurt).

@staab commented on GitHub (Jun 9, 2022): Just wanted to register my support for this — on my Pixel 3 the UI is extremely slow even after only a few pages of scrolling. I was just looking into this, but it looks like @heikomat has already made a ton of progress! One thing I might add after a brief look through [this file](https://github.com/photoprism/photoprism/blob/develop/frontend/src/pages/photos.vue) is that line 552 might be the source of the high CPU usage: `this.$forceUpdate();` — simply removing that might significantly reduce the need for virtualization (though that certainly wouldn't hurt).
Author
Owner

@heikomat commented on GitHub (Jun 9, 2022):

@staab i'll later take a look at the difference that forced update makes. Virtualized or not, removing it might improve performance (if it is actually not required)

But even if we remove that line, a core problem is the sheer amount of vue-elements an pictures rendered without virtualization.
Having this many elements and pictures in memory means that the view uses (very roughly) about 1GB/Ram per 1000 scrolled pictures.
With the virtualization and performance improvements the ram-usage barely goes over 400MB even after scrolling over 3000 pictures

@heikomat commented on GitHub (Jun 9, 2022): @staab i'll later take a look at the difference that forced update makes. Virtualized or not, removing it might improve performance (if it is actually not required) But even if we remove that line, a core problem is the sheer amount of vue-elements an pictures rendered without virtualization. Having this many elements and pictures in memory means that the view uses (very roughly) about 1GB/Ram per 1000 scrolled pictures. With the virtualization and performance improvements the ram-usage barely goes over 400MB even after scrolling over 3000 pictures
Author
Owner

@heikomat commented on GitHub (Jun 9, 2022):

@srett i tested scrolling through images with that forced updated and without it, with virtualization and without it.
The existence of the forced updated does not seem to make a noticable difference either way.

But! You said you use a Pixel 3. That happens to be my daily driver aswell, so here's a sneak peak at what might be possible with the virtualization and performance fixes.

This following screencapture is made on real hardware, and it might even be a little faster on a real deployment (because of faster api-calls). The pictures are blurred by sacling them up 3000% because they are personal pictures.

https://user-images.githubusercontent.com/20770029/172920269-16d1b765-e2f3-4fd7-8f8f-cf0d2b1d9381.mp4

Edit: please do not bug the maintainers about when these things will get merged. They are currently occupied with more important stuff around phototropism to ensure smooth sailing for the future

@heikomat commented on GitHub (Jun 9, 2022): @srett i tested scrolling through images with that forced updated and without it, with virtualization and without it. The existence of the forced updated does not seem to make a noticable difference either way. But! You said you use a Pixel 3. That happens to be my daily driver aswell, so here's a sneak peak at what **might be** possible with the virtualization and performance fixes. This following screencapture is made on real hardware, and it might even be a little faster on a real deployment (because of faster api-calls). The pictures are blurred by sacling them up 3000% because they are personal pictures. https://user-images.githubusercontent.com/20770029/172920269-16d1b765-e2f3-4fd7-8f8f-cf0d2b1d9381.mp4 Edit: please do not bug the maintainers about when these things will get merged. They are currently occupied with more important stuff around phototropism to ensure smooth sailing for the future
Author
Owner

@heikomat commented on GitHub (Jun 29, 2022):

yup, pseudo-virtualization and scroll-performance improvements have been released.
Happy to close this issue 😊

@heikomat commented on GitHub (Jun 29, 2022): yup, pseudo-virtualization and scroll-performance improvements have been released. Happy to close this issue 😊
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/photoprism#1408
No description provided.