-
Notifications
You must be signed in to change notification settings - Fork 294
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
[Perf] Run scroll and frame handlers outside of angular zone #117
Conversation
src/virtual-scroll.ts
Outdated
parentScroll.addEventListener('resize', this.refreshHandler); | ||
} | ||
this.zone.runOutsideAngular(() => { | ||
parentScroll.addEventListener('scroll', this.refreshHandler); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use Renderer2
instead of direct DOM manipulation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
// update the scroll list | ||
this.viewPortItems = items.slice(start, end); | ||
this.update.emit(this.viewPortItems); | ||
this.zone.run(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this.zone.run()
is nested inside an if
, meaning that on the else
case we won't run the zone. In such case, I think scrollHeight
and topPadding
won't be updated on the template because no CD will run. Using zone.run()
outside the if
would kill the perf so the best solution is probably to remove the bindings and use Renderer2
to set the value programmatically.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this.scrollHeight
and this.topPadding
only change to new values if the start and end change, so I think it's okay. When the there is an actual change, the zone.run will cause CD which will detect the property changes.
Correct me if I am wrong here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They might also change if the items array changes, which would already cause a CD cycle.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm don't fully understand your computations I just saw that you're setting this.scrollHeight
and this.topPadding
before the if
and their values seemed unrelated to start
and end
.
Here is the use case I'm concerned about :
You can scroll a bit and keep the same items displayed (so same start
and end
) while you still need to show that scroll by moving the items a little (which seems to be done with topPadding
).
If this.scrollHeight
and this.topPadding
are not important on the else
case it's even better for perf to not sync their value with the DOM so all good then.
To optimize a little more the scroll, it would be interesting to add the deletion of the hover during the scroll. Applicable for those like me who use the hover on content. https://css-tricks.com/turn-hovers-scroll/ An example: private scrollHovering = () => {
clearTimeout(this.scrollTimer)
if (!this.contentElementRef.nativeElement.classList.contains('table-disable-hover')) {
this.contentElementRef.nativeElement.classList.add('table-disable-hover')
}
this.scrollTimer = setTimeout(() => this.contentElementRef.nativeElement.classList.remove('table-disable-hover'), 200)
this.refresh()
}
private addParentEventHandlers(parentScroll: Element | Window) {
if (parentScroll) {
this.zone.runOutsideAngular(() => {
parentScroll.addEventListener('scroll', this.scrollHovering)
if (parentScroll instanceof Window) {
parentScroll.addEventListener('resize', this.refreshHandler)
}
})
}
} |
4521246
to
8f45965
Compare
@johaven the scroll class is out of scope for this PR, maybe add it as an issue, after this PR goes in you could use Renderer2 to toggle a |
@ghetolay I've updated it to use Renderer2 for the height and transform styles. What do you think? |
Looks fine to me now. Maybe you could avoid manipulating the DOM is it's to set the same value. |
Yeah, we could save some cycles. Apparently, setting CSS to the same value does incurr a small cost |
Just an idea that i applied in my code : it seems to be lighter to execute calculateDimensions function only on resize events to optimize the refresh function speed. |
@johaven I think |
Without unit tests, I don't want to make too many change in a single PR. The other perf things could be done in another PR. Also, I started working on some unit tests, but I don't have a heap of free time at the moment :) |
My code: refresh(updateDimensions: boolean = false) {
this.zone.runOutsideAngular(() => {
if (updateDimensions || !this.dimensionsView) {
this.calculateDimensions()
}
requestAnimationFrame(() => this.calculateItems())
})
}
...
private refreshWithDimensions = () => {
this.refresh(true)
}
...
this.eventResizeHandler = this.renderer.listen('window', 'resize', this.refreshWithDimensions)
...
if (start !== this.previousStart || end !== this.previousEnd) {
...
if (this.startupLoop === true) {
this.refresh()
} else {
this.change.emit({start, end})
}
})
} else if (this.startupLoop === true) {
this.startupLoop = false
this.refresh(true)
}
...
ngOnChanges(changes: SimpleChanges) {
this.previousStart = undefined
this.previousEnd = undefined
const items = (changes as any).items || {}
if ((changes as any).items !== undefined && items.previousValue === undefined
|| (items.previousValue !== undefined && items.previousValue.length === 0)) {
this.startupLoop = true
}
this.refresh(true)
} |
@ghetolay updated, now it only updates styles when their respective value has changed |
@rintoj over to you, I think this is ready to merge. |
@tstirrat, Thanks for your contribution. While testing this PR, I found that the component is breaking when items array is an empty array. The scroll is indicating that we have more items in the list. |
@tstirrat, found the root cause. I will fix. |
To improve perf, attach the scroll/frame handlers outside of the zone. Only go back into the zone if there is an actual change to be emitted.
[parentScroll]
is supplied.To highlight the difference in the demo app, I scrolled just a tiny bit so that no actual changes need emitting:
Before:
After:
Once a change is emitted, we must do this in the
angular
zone and incurr the change detection, so the difference is only a small reduction in the initial scroll handler time:Before:
After:
Fixes #66
/cc @ghetolay