Goal : be jank-free at 60fps
Jank is when missed frame appears when navigating a website, you don’t want that. http://jankfree.org/
You can find a lot of talks and articles from Paul Lewis, Tom Wiltzius and Nat Duca, who are great speakers btw, that explain clearly how to avoid jank, how to diagnose it, and how to fix it. Check the resources at the end of this post to have some pointers.
I decided to resume quickly everything I learned from a lot of different sources in order to not do those mistakes again and help you and me to understand what can cause this jank. I don’t explain in details everything, every term I use, check out the resources of the post to have a better understanding if needed.
I’ll start with general javascript concepts and then I’ll continue with deeper concepts such as the layout/painting/layers processes the browser do and how to optimize them, then I’ll finish with some network advices.
Some of the items are maybe not relevant anymore nowadays for certain browsers because it was optimized since. I tried to filter them but still.
Feel free to correct me if I’m wrong.
Object instances
If you have a bunch of object to create, forget about module pattern, prototypes are faster to create, or create plain simple object. http://jsperf.com/prototypal-performance/55
Holey array
Don’t do holey array (it can be converted to a dictionary internally thus being way slower). Try to not use the delete
keyword. It can change on the fly the internal class being used for the object you just deleted from (for instance, an array with a delete
in the middle can transform it into a map). http://jsperf.com/packed-vs-holey-arrays
Objects pooling
If you are creating and destructing a lot of objects/references, consider using an object pool to avoid triggering the Garbage Collector (which would make you lose some precious ms at random times). Check http://www.html5rocks.com/en/tutorials/speed/static-mem-pools/ for more details.
Image resizing
Resize your image exactly as the size you are using in your page (we are not talking about multi-support here) to avoid the browser to resize the image itself (cpu consuming). Consider using srcset
on
or use
if you want to handle high DPI devices.
Background-size
Beware of background-size
, keep the image at the original size instead of asking the browser to resize on the fly. Create multiple version of the same image if needed. You can also create a spritesheet image of your images to do only one HTTP request (and maybe have a faster memory access but don’t take this into consideration, that’s way too insignificant to be considered).
CSS gradients
Beware of repeating gradients. That slows down a lot the browser rendering. For instance, if you put it on the background of your page in which you can scroll. Prefer to use a tiled background image.
Custom fonts instead of image
Don’t forget that you can use nice custom fonts instead of images to draw symbolish things. Such as FontAwesome. You will save bandwidth and designer time. http://fortawesome.github.io/Font-Awesome/icons/
Or you can use unicode characters (it has a lot of icons, not only characters) or svg (light and scalable).
Image lazy loading
Consider using an image lazy loading plugin to load images only if the user can see them of if everything else has been loaded first. If by default the image is not displayed in the first view at loading time, you can defer its request when the user is scrolling into for instance.
If you have a carousel of images, consider to not load every images of the carousel at the beginning. Either wait the user to hover it or load them after everything else is loaded. If the user does not even use the carousel, it’s useless to load the other images.
Event listeners
Try to not bind event listeners on the whole document just to handle the event on a deep child. Try to bind listeners on the element you need or a nearest parent. Otherwise the compositor
(a thread dedicated to handle inputs and scrolling) will be triggered for nothing each time the event will happen on the page, that could have no link with your element.
Memory management / variable scoping
Try to not use global variables. Release what you can when you can. With ES6, use only let
, no more var
. Remove the event listeners you don’t need. Be especially careful with closures, they can contain a lot of references that will never be released if you are careless.
Events throttling
Throttle the events that can be called numerous times : onscroll
, onmousemove
, onresize
, ondragover
. They will be called only a few times and that will be enough.
requestAnimationFrame() is your best friend
Do not use setTimeout()
or setInterval()
to throttle event handlers such as onscroll
or onresize
. Use requestAnimationFrame()
to only change the visual state when a new frame is going to be rendered, to not compute things that won’t be render in-between 2 frames. Check http://www.html5rocks.com/en/tutorials/speed/animations/ for more details.
Event handlers logic
Don’t do any complex logic in the event handlers. Just save somewhere the values which you care of the current state (scrollTop
, mouse position…), and let the browser pursues its events pipeline. If you are doing something heavy in a handlers, you will delay every other events to be handle and will delay the rendering (then you call a requestAnimationFrame(fn)
to use those saved values). Try to bind global events only once, such as onscroll
or mousemove
. Just save their positions in those handlers and when you need them, just call functions in requestAnimationFrame()
using those saved values.
:hover and scrolling
Be careful with :hover
effect on elements in the middle of a scrollable page. If the user scrolls using its mousewheel on top of those elements, they will trigger the :hover
at the same time the scrolling is occurring. That can slow down dramatically the fps and you end with jank (missed frames during scrolling becausee of a lot of restyle/paint/composite layers operations). You can for instance add a class on the container when it’s scrolling to not trigger the :hover
effect while it’s present.
Dynamic class adds
Don’t add classes on top of your document if that only change an item deep in the DOM (that could cause a full repainting from the parent). Try to target specifically the element or a near parent.
Avoid Layout Trashing
When you need to read some positions or sizes from the DOM (.offsetTop
, .offsetWidth
…), don’t alternate them with writes of positions or sizes on the DOM (element.style.width = width
). That would cause what is called “Layout Trashing” because each time you change a size or position on the DOM, that triggers a forced synchronous layout (painting). Do all your readings at the beginning, store them into variables, then do your writings. You can call a requestAnimationFrame()
to read those values, then inside, call another requestAnimationFrame()
to write your transformations.
To know if have to kind of cycles, check your timeline in the debugger and look for cycles of :
– recalculate style
– layout
– recalculate style
– layout
You can know what property triggers what on http://csstriggers.com/ (only Chrome for now)
transformZ(0) aka the null transform hack
There is a trick called the “null transform hack” to force the browser to create a distinct layer containing a DOM element that will be painted using the hardware acceleration (and independently of the other layers) using transform: translateZ(0)
or transform: translate3d(0,0,0)
. Those statements changes nothing visually but on a desktop computer, that can really boost your fps. If you combine that on elements that have a shadow (like, a lot on your page), you clearly see the difference when playing with. But be careful, mobiles do not have the same capability than desktops, and the performances could be worse.
Will-change
Instead of this hack, consider implementing the fairly new css property will-change: [transform|scroll-position|contents|
to hint the browser to create a distinct composite layer for this element. You just notify it that the particular property will probably change for this element. The browser will try to optimize what it can (ie: create a new layer for it). But don’t abuse it to not create another problem (it’s useless to do * { will-change: * }
the browser is already trying hard).
If you know you are going to change the text of your item, you can set will-change: contents
. Check https://dev.opera.com/articles/css-will-change-property/ for more details.
Remove this property when you are done with the change. Do not apply it on a global rule, just apply it when just before it’s needed. For instance, listen to the :hover
state of its container to set it on the child using css : no hover on parent = no will-change on child. Or set it using javascript and reset it to auto
when you’re done.
Composite layer
What creates a new composite layer is the when you use translationZ/3d, and
(they are automatically embedded in a new layer), animations with opacity and transform, and some css filters that use hardware acceleration. It has nothing to do with z-index layers. Check http://www.html5rocks.com/en/tutorials/speed/layers/ for more details.
Animation/Transitions
Do not animate with javascript. Use css animations/transitions. The animations will be processed by another thread than the main one. Even if your js does complex calculations and slows down the main thread, the css animation will still be smooth.
Transform to animate position
When you have element which position is dynamic, do not use css top/left, use transform: translate
. top/left should be reserved for initial positioning only, just to organize the layout. css transform
is especially done for transformations, it does not reflow/redraw whereas top/left does. In DevTools, enable Show Paint Rectangle
(Timeline > Rendering) to see what is repainting on your page.
Moreover, the translate transform animation is using subpixel rendering instead of pixel per pixel for top/left animation. The rendering is smoother.
FLIP
It’s a technique used to animate an element the cheapest way possible when the positions/dimensions are unknown/dynamic. Paul Lewis explains it:
Basically, it stands for: First, Last, Invert, Play
. From the initial state, you set directly its last state, read some values from it (position, size), apply an inverted transform style to put it back where it was, and clear the transform style to trigger the animation.
Fixed elements
If you have a top bar in fixed position, consider to put it in its own layer. Otherwise, scrolling the content underneath cause the element to be repainted each time. You can check with the Show Paint Rectangle
toggle.
Chrome DevTools <3
In Chrome DevTools, you can press h
on an element to toggle its visibility. That avoids you to add a display: none
manually on its style. Life saver. Of course, their performance tools is uberly useful and you can use about:tracing
if you are desperate (and you will probably be more desperate with that ;-).
To check if you have some weird or expansive painting behavior, enable the continuous painting mode and toggle elements visibility to see which one(s) is(are) causing the problem : Timeline > Rendering (press Show paint rectangles
+ Enable continuous page repainting
. You can enable the fps meter too. Now, scroll into your page, do some actions, hide some elements to see if you can find the culprit (if you notice a boost).
Multiple adds to the DOM / DocumentFragment
When you want to append several items to the DOM, use document.createFragment()
. It’s available in all browsers since a while (jQuery internally use it when you call append()
with an array for instance). You can use traditional DOM operations on it (appendChild
, style
etc.) without the browser to render it while you create it. Then when you are done, you just append it to the real DOM in one shot.
Avoid to read value from CSS
Try to not use getComputedStyle()
(or .css()
getter from jquery, it’s using that internally) to read value from CSS directly, that’s calling the “Recalculate Style” process. Save the values you need before somewhere if you can and use them, or if it’s for animate something, use css transition/animation.
getBoundingClientRect()
This magic function returns in one call the position and the dimension of the item it’s called on : height, width, top, left, bottom, right. Use it once when you need to read those differents values.
User reaction
When a user clicks on a button, you have about 100ms before doing any animation. The user won’t notice anything before, it’s its reaction time. Therefore, you can prepare anything you need (get sizes, positions…) and have an ugly 100ms frame if you need, he won’t notice. ;-)
If you want to deal with mobiles, add in your
. That could enable the GPU rasterization on the phone (Androids). But basically, add that even if it’s not an Android, that won’t do harm on the contrary.
DOM element count
Try to not have too many DOM nodes on your page. For instance, 1000 for a mobile app is a good number. (desktop version : 4000 twitter.com, 2500 for amazon or facebook, mobile version: 1000 for m.twitter.com, 1700 for m.facebook)
CSS stylesheets
CSS are blocking resources for the browser. It waits for them to be loaded and parsed before rendering anything to avoid a flickering effect. So, keep it light, avoid to load unnecessary css rules. You can split your stylesheet by media for instance. The browser won’t wait for them if their rule does not apply :
Put your CSS in the
for the browser to request them as soon as possible.
Avoid to use @import
in stylesheets to not block again the browser (because it will request again a stylesheet).
Async JS scripts
Inline or
blocks the DOM construction when the browser encounters them, which will therefore delay the initial render. Generally, you put your
at the end of the body to render it directly, and because the scripts often references DOM parts (in
nothing is constructed yet).
You can add the async
flag on the tag to defer its loading. The browser won’t block anything and will pursue its process.
Page load time and runtime performance
You have to be careful to these 2 metrics. The first one is the time to get the initial rendering of your website. The other is the performance when the user is using it.
At loading, ensure to not have too many redirects, each one of them add noticeable delay : xxx.com => m.xxx.com => m.xxx.com/start
Also consider about gzipping the content your server sends. You can easily reduce by 80%-90% the size of your js/css (because they are text based, gzip does miracles).
Browser loading events
You need to understand the events the browser expose when it’s accessing your website. The most known is DOMContentLoaded, this is the one that is called when every resources has been requested and parsed (js/css) but not processed yet. Then ‘loaded’ is when every resources has been processed (even images). Those events need to be as short as possible.
To have an explanation on each of those events, check out
If your browser implements the performance api, you can get the timing with some javascript: http://googlesamples.github.io/web-fundamentals/samples/performance/critical-rendering-path/measure_crp.html
Your browser extensions have a great impact on that! If you have AdBlock for instance, check with and without it (eg: use the private mode without any extension loaded).
Or, you can check the network tab in devtools to have those numbers directly.
Cache your resources
Use the browser cache system to avoid transmitting each time your static resources : use ETag (browser will then use If-None-Match) and Cache-Control flags (for the browser to cache it locally for instance) . Explanations on
Don’t delay the navigation
Don’t delay the unload
of your page, when navigating to another page for instance (not just when the user closes the tab). Instead of sending a classic ajax and add some trick to wait (as a empty for loop !) use navigator.sendBeacon
, it exists for that.
Profiling tools
- Use the developer tool, it’s very, very handy.
-
chrome://tracing
to see what’s under the hood when you are desperate - to have a first review of your website performance and hints to improve it
-
http://www.webpagetest.org/ to test your website using different configuration/location : try to have a speed index around
1000
for a mobile version.
References / Sources
http://jankfree.org/
http://www.html5rocks.com/en/tutorials/speed/layers/ | Tom Wiltzius
http://www.html5rocks.com/en/tutorials/speed/scrolling/ | Paul Lewis
http://www.html5rocks.com/en/tutorials/speed/animations/ | Paul Lewis
http://www.smashingmagazine.com/2012/11/05/writing-fast-memory-efficient-javascript/ | Addy Osmani
| Ilya Grigorik
https://dev.opera.com/articles/css-will-change-property/ | Sara Soueidan
http://wilsonpage.co.uk/preventing-layout-thrashing/ | Wilson Page
| Paul Lewis
http://www.html5rocks.com/en/tutorials/speed/static-mem-pools/ | Colt McAnlis
https://vimeo.com/77591536 | Paul Lewis | A developer’s guide to rendering performance
| Paul Lewis
| Paul Lewis
http://csstriggers.com/ | Paul Lewis