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

[Perf] Run scroll and frame handlers outside of angular zone #117

Merged
merged 6 commits into from
Oct 4, 2017

Conversation

tstirrat
Copy link
Contributor

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.

  • There is also a slight fix here where we don't attach the host element scroll listener if [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:
screen shot 2017-09-16 at 1 40 33 pm

After:
screen shot 2017-09-16 at 1 51 20 pm

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:
screen shot 2017-09-16 at 2 06 08 pm

After:
screen shot 2017-09-16 at 2 06 43 pm

Fixes #66

/cc @ghetolay

parentScroll.addEventListener('resize', this.refreshHandler);
}
this.zone.runOutsideAngular(() => {
parentScroll.addEventListener('scroll', this.refreshHandler);

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do

Copy link
Contributor Author

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(() => {

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.

Copy link
Contributor Author

@tstirrat tstirrat Sep 27, 2017

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.

Copy link
Contributor Author

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.

Copy link

@ghetolay ghetolay Sep 27, 2017

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.

@johaven
Copy link

johaven commented Sep 25, 2017

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)
                }
            })
        }
    }

@tstirrat
Copy link
Contributor Author

tstirrat commented Oct 2, 2017

@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 .scrolling class on the container element.

@tstirrat
Copy link
Contributor Author

tstirrat commented Oct 2, 2017

@ghetolay I've updated it to use Renderer2 for the height and transform styles. What do you think?

@ghetolay
Copy link

ghetolay commented Oct 2, 2017

Looks fine to me now.

Maybe you could avoid manipulating the DOM is it's to set the same value.
Dunno how often this could happen and how much we would gain.

@tstirrat
Copy link
Contributor Author

tstirrat commented Oct 2, 2017

Yeah, we could save some cycles. Apparently, setting CSS to the same value does incurr a small cost

@johaven
Copy link

johaven commented Oct 2, 2017

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.

@tstirrat
Copy link
Contributor Author

tstirrat commented Oct 2, 2017

@johaven I think calculateDimensions also needs to be run when items.length changes.

@tstirrat
Copy link
Contributor Author

tstirrat commented Oct 2, 2017

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 :)

@johaven
Copy link

johaven commented Oct 2, 2017

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)
    }

@tstirrat
Copy link
Contributor Author

tstirrat commented Oct 2, 2017

@ghetolay updated, now it only updates styles when their respective value has changed

@tstirrat
Copy link
Contributor Author

tstirrat commented Oct 2, 2017

@rintoj over to you, I think this is ready to merge.

@rintoj
Copy link
Owner

rintoj commented Oct 4, 2017

@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.

image

@rintoj
Copy link
Owner

rintoj commented Oct 4, 2017

@tstirrat, found the root cause. I will fix.

@rintoj rintoj merged commit faf636a into rintoj:master Oct 4, 2017
@tstirrat tstirrat deleted the angular-perf-fix branch October 12, 2017 21:47
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

Successfully merging this pull request may close these issues.

Just an idea for performance
4 participants