Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Question regarding CPU usage when idle #68

Open
kiejo opened this issue Jul 5, 2023 · 7 comments
Open

Question regarding CPU usage when idle #68

kiejo opened this issue Jul 5, 2023 · 7 comments

Comments

@kiejo
Copy link

kiejo commented Jul 5, 2023

I'm currently looking at different virtualization libraries and so far really like this one. But after experimenting a little more with it, I noticed that there is always some CPU usage going on even when nothing is being scrolled or changed. I noticed that this is caused by requestAnimationframe continuously firing and doing work.
Now my question is whether this library could be optimized to skip this work when nothing is changing or whether this is somehow inherent to the technical approach of this library. I haven't looked into the implementation details yet, but haven't run into the same issue with other virtualization libraries.

@oleggrishechkin
Copy link
Owner

oleggrishechkin commented Jul 5, 2023

Hi, @kiejo
Most other virtualization libs use simple onscroll event. I tried some approaches with onscroll/intersection observer/non endless animation frame but unsuccessfully.

I think intersection observer for spacers (a div which fills virtualized space) can be used and I made some drafts with it a time ago. Mostly it works fine. I want to move to this solution in future if I'll make it good in all cases.

I need a way to know when elements changed their positions near viewport - it can be scroll, parent resize, shifts by elements added on top/bottom, window resize, item height change... Endless request animation frame handle all of these "events".

@kiejo
Copy link
Author

kiejo commented Jul 5, 2023

Thanks for the quick answer and explanation! Using an intersection observer that spans the list's viewport sounds like a good potential approach to detect when elements move out of or into the viewport. I will have to look at the current implementation in more detail to better understand how everything works at the moment.
It's great to hear that it's a topic that is already on your radar and that might get addressed in the future. Thanks for the great library!

@oleggrishechkin
Copy link
Owner

oleggrishechkin commented Jul 5, 2023

@kiejo I can explain current logic deeply.

Every frame I compute rects of spacers, first/last rendered list items and viewport bounds. Then I check is everything shown inside viewport without free spaces. If I need to render more items, I change "indexes to render", but it's some kind of estimation - at next frame indexes may be corrected.

Logic now: render some items -> check that all items fill viewport -> correct indexes to fill viewport.

When you scroll last item can be fully intersect the viewport - this is the moment I change indexes for example.

@kiejo
Copy link
Author

kiejo commented Jul 5, 2023

That's an interesting approach. It sounds like an IntersectionObserver with the list container as its root could be used to detect whenever a spacer element intersects (or stops intersecting) with the list rect. Whenever the spacer intersects, more items would need to be rendered until the observer fires again indicating that the spacer does not intersect anymore and got pushed out of the list rect. The rootMargin of the observer could be used to handle an overscan so that new items are rendered sooner.
A similar mechanism could be used to detect when items need to be removed. A second observer with a wider rootMargin could be used to check when a spacer element gets pushed too far out of the list rect. That would indicate that items can be removed from the DOM.

I haven't tried any of this, but in theory I think this could potentially handle cases like scrolling, parent/window resize, new elements being added within the rendered range, and height changes of rendered items.
I'm sure there are many more cases to handle and and an approach like this would need more exploration and prototyping to verify if it's actually viable. I also don't know what the overall performance would then look like, but the idle CPU usage should definitely be lower if this approach works.

It sounds like you may have already thought about something like this as you mentioned you already made some drafts using IntersectionObserver. Just thought I'd share some ideas in case you haven't thought about some of these yet.

@oleggrishechkin
Copy link
Owner

@kiejo Yes, I made exactly you described with IntersectionObserver. The problem with observer is it fired only once - when "intersection" changed. So bad case is when spacer intersect viewport, we render new items, but new rendered items not fill the free spaces and spacer still intersect viewport - at this case virtualization will be broken. I fix it by reobserve anytime on indexes change (this is a part where rAF is more performant) - but this sulution has some pitfalls too. Also I tried to use "check" on layout effect instead of reobserve on index change and catch max state update error in react sometimes 😅

Anyway I think intersection observer is a good way to "make it right" without hacky rAF.

@kiejo
Copy link
Author

kiejo commented Jul 6, 2023

Good to hear that you already tried this approach :)

The problem with observer is it fired only once - when "intersection" changed. So bad case is when spacer intersect viewport, we render new items, but new rendered items not fill the free spaces and spacer still intersect viewport - at this case virtualization will be broken.

Maybe something like this could help with this case:
Whenever the observer gets notified about the spacer intersecting, start an update loop that keeps adding more items until the observer gets notified that the spacer does not intersect anymore.

function update() {
  // Needs to stop the update loop when the start/end of the items array is reached and the
  // items do not take enough space to push the spacer element out of the list rect (prevent
  // endless update loop).
}

let frameId = null
function updateLoop() {
  frameId = requestAnimationFrame(updateLoop)
  update()
}

function startUpdateLoop() {
  if (!frameId)
    updateLoop()
}

function stopUpdateLoop() {
  if (frameId) {
    cancelAnimationFrame(frameId)
    frameId = null
  }
}

const observer = new IntersectionObserver(entries => {
  if (entries[0].isIntersecting)
    startUpdateLoop()
  else
    stopUpdateLoop()
}, {
  root: listElement,
  threshold: 0,
})

observer.observe(spacerElement)

This is just some ideation on how this could potentially work and I'm sure the logic would need to handle more cases.

@oleggrishechkin
Copy link
Owner

@kiejo , good suggestion (thought about it, but not tried)

Also different threshold of intersection can be used with really small steps - like [0.001, 0.002..., 1] to catch shifts/changes in intersection - but intersection handler can evaluate frequently and not cover cases where 0.001 is hundreds of pixels.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants