Writing a post about CSS performance, or performance of any type, opens one up (quite rightly) to people questioning the performance of your own site.
In the comments of a recent post, an interesting comment was posted by Mailopl:
Mate, you should seriously reconsider your layout. You write here about performance, but onscroll you fire get pageYOffset() that takes about 400–800ms causing whole page to junk as hell xD
Now, I pointed out my post was about CSS performance and that my JS skills are lacking but I’d be keen to do whatever I could. Further comments suggested using
position: fixed; on the elements but I already was. They also suggested using a debounce function in the JS – I already was.
Furthermore, testing the site on my Retina MacBook Pro (or indeed iPad or Android tablet – the devices I had to hand) never caused any kind of jank. I had no doubt those people were seeing what they were seeing but I couldn’t consistently replicate the issue. When you can’t replicate a problem, it’s difficult to fix it! Weird.
I opened up the Chrome Dev Tools to investigate and this is where things got even stranger.
Show paint rectangles
Performance centric developers will be aware that Chrome Dev Tools has a ‘Show paint rectangles’ mode. Enabling this puts a red border around areas that are being repainted by Chrome as you interact with the page. I could see large paint rectangles on scroll on a Mac Pro but on my Retina MacBook Pro (the location I had developed the site) nothing – it behaved exactly as I would hope.
Big bad rectangles
On a page like this (if you’re reading this on a large enough viewport), you’ll see two circles top left and right (word count and days since post last updated). On the Mac Pro, whenever the page was scrolled (with paint rectangles enabled) a big bad red paint rectangle went from the left hand circle right across the page to encompass the right – a significant area of the screen. Every scroll, there it was. 🙁
I started re-reading some anti-jank articles to see what I was missing.
The union of damaged regions
… Chrome does a union of damaged regions
I missed this vital point at first. The key thing to understand is that if Chrome/WebKit decides that a couple of page areas need repainting (on the same compositing layer) it will create a union from the separate areas. So, what you might expect to be painted as two separate paint areas actually becomes painted as one. The problem being, in my case, the two areas were on opposite sides of the screen – leading to one enormous paint area whenever the page was scrolled.
But why were these areas being painted anyway? The HTML5 Rocks site, that I was reading about this on also had a fixed position area but no red paint rectangles showed there on scroll. What was the secret sauce they were using?
I’ll get straight to the point. They were adding
backface-visibility: hidden; to the fixed position elements. That was stopping the paint happening on scroll. So, I had a nice simple solution for my own site but I was annoyed I didn’t understand WHY that worked: I had my suspicions but no actual proof. In these situations I always do the same thing; ask someone way smarter.
Who better to ask than the author of the jank-free scrolling article? I sent an email to Paul Lewis, describing the issue and he responded swiftly and in full detail. With his permission, here is the pertinent information he replied with:
… when elements repaint, the dirty rectangle calculation is done per layer. So if an element is on its own layer then it won’t affect anything else. If you promote a fixed header – say – then when content appears at the bottom of the page there is only the new content that needs to be painted. Without the promotion the header needs to be repainted at the top of the page. You might wonder why we don’t automatically promote fixed position elements. The answer is: we do for high DPI screens, but we don’t for low DPI because we lose sub-pixel antialiasing on text, and that’s not something we want to by default. On high DPI screens you can’t tell, so it’s safe there.
Now, although Paul didn’t know it, he’d answered two further questions I would have asked. Firstly, why don’t fixed position elements get automatic promotion to a new layer and secondly, why I hadn’t seen the issue when developing the site (on a high DPI Retina MacBook Pro).
If I had thought about it, I could have opened Chrome’s emulation mode (open Dev Tools, press escape and click the ‘Emulation tab) and set the DPI of my Retina MacBook Pro down to ‘1’ (Alex Gibson’s smart thought, confirmed as a ‘should work’ by Paul Lewis) – in that mode the bug did show (albeit it still appeared differently than it did on the Mac Pro – green rectangles of a different size than the red ones on the Mac Pro but there none the less). This is another thing to consider in future if a LoDPI screen isn’t to hand for actual device testing.
will-changethat I first read about via Tab Atkins. It aims to allow authors to indicate to the UA when they know an element ‘will-change’. This property will let authors hint to the UA that the element needs special treatment (in the same way we do with
translate3d(0, 0, 0)now). Again, Paul Lewis has some great info on this. However, I have to say I’m not convinced. While the use case is clear (there are situations when a UA can’t possibly know what might happen to an element and this allows an author to ‘prime’ it), there are still plenty of times when using such a property would still be used to coerce the UA into doing something it should be figuring out already. After all, as in the problem I am describing in this post, adding
will-changejust doesn’t seem appropriate to the situation – after all, the whole point for me was that the elements weren’t changing.
Getting jibes about your own work not being up to par, as long as delivered in relatively good form, is never a bad thing. It’s generally pointing out some shortfall you weren’t aware of. If you can make improvements you’ll probably learn something along the way (I have).
More practically, if you’re using fixed positioning elements on a page, ensure you also add a property & value pair that will promote the element to its own layer (transform3d or backface-visibilty for example). This will prevent WebKit/Chrome re-painting it on scroll.
Finally, if you possibly can, do things like frame-rate testing on as many different devices and platforms as possible.