60fps scrolling article got quite a few people excited but also a few smart people to point out some flaws in the approach. I was planning to just update the original article but I’ve compiled so much info about this topic that I thought it would be better to do a follow up article and dive into the detail of the techniques that I tried and some solutions to the critiques made about the last technique.
Take a look at the final test case that performs the best, watch the screencast and checkout the 60fps-scroll repo. Read on to learn why.
There were two valid critiques that were brought up:
The original idea of disabling pointer-events
on the body gives you the illusion that scroll performance is great but let’s face it simple sites don’t really have scroll jank issues. Where we do see flaws in the original technique is when we it’s used on more complicated visually rich sites. By adding a class to the body we force the browser to do a style recalculation which is costly on the body as it has to traverse the whole tree to make sure this top level class doesn’t effect any children.
The above image is a timeline snapshot of the more complex DOM using the older technique of changing the class on the body, as you can see this can just blow away your pursuit of 60fps scrolling before you’ve even began.
On mobile or touch based devices this technique really has no effect as this is only relevant for mouse cursors. Running the test case in an iPhone works as normal when scrolling.
The original test case was very janky in Firefox and quite slow. Without the equivalent timeline tool it’s hard to say what the browser is exactly doing on scroll. The revised technique performs a lot better as it doesn’t trigger any repaints or high style recalculations.
IE11 and Firefox both turn off hit testing on scroll if the users mouse doesn’t move. Safari I’m not sure what it does.
One of the critiques was what if the user clicks on something as they’re scrolling or clicks something before the timeout has ran once the user stops scrolling.
We can semi-solve this via the use of elementFromPoint
and triggering synthetic events. This won’t solve every edge case but it will get you mostly there.
// capture all clicks and store x, y coords for later document.addEventListener('click', function(event) { if(event.target === cover && !event.synthetic) { pos.x = event.clientX; pos.y = event.clientY; clicked = true; } }, false); function dispatchClick(coords){ var event = document.createEvent("MouseEvent"), elem = document.elementFromPoint(coords.x, coords.y); event.initMouseEvent( "click", true /* bubble */, true /* cancelable */, window, null, coords.x, coords.y, 0, 0, /* coordinates */ false, false, false, false, /* modifier keys */ 0 /*left*/, null ); event.synthetic = true; elem.dispatchEvent(event); }
By attaching a listener to the document we can capture all clicks that happen before the timeout fires by storing the cursor x, y coordinates when the timeout fires we can target the element via elementFromPoint and then trigger a synthetic event on that element.
// On timeout check to make sure the user actually clicked during scroll // Dispatch synthetic click event setTimeout(function(){ //... if(clicked) { dispatchClick(pos); clicked = false; } },500);
The downside to this approach is this will only trigger listeners attached via JavaScript it won’t trigger an actual click so if I was to click on a form submit button it wouldn’t do anything. This won’t work on iframes either so things like social button embeds etc. Of course once the timeout has fired all clicks will worked as expected.
One suggestion in the comments was to use a small cover div that follows the mouse cursor as the user scrolls and use CSS transforms to move it on the viewport so it won’t cause any layouts. I found the performance degraded compared to the original pointer-events
technique and it also oddly triggered layouts/paints when changing the translate property but not all the time. Perhaps my test case is flawed? If you know how to fix it let me know.
Check out the test case for yourself. Note that this demo only alters the webkitTransform
property so it won’t do anything in Firefox.
We’ll dive into the following test case that uses a cover div and pointer-events: none
and go through some optimisations to improve it further.
One of the main things when working with lots of images is not resizing them through CSS or the img elements width/height attributes as the browser has to decode the image and resize it before painting it to the screen. This makes sure your scrolling performance isn’t hit hard by big repaints. Harder to do when catering for High DPI screens but definitely a solution that the picture element will solve in the future.
I created a little library that wraps up this technique that you can drop into your site and see if it helps your scroll performance.
Some may have the feeling that this may be a case of the premature optimisation gremlins and they’re right. I wouldn’t get caught up on 60fps performance until you have a problem with your scrolling where it can really effect the end users experience within your site.
Post filed under: css, javascript.