Written by Ramón Saquete
Table of contents
- 1.1 Browser caching
- 1.2 Compressing with Brotli q11 and Gzip
- 1.3 Minifying
- 2.1 Avoid using too much memory
- 2.2 Prevent memory leaks
- 2.3 Use web workers when you need to execute code requiring a lot of execution time
- 2.4 Use the Fetch API (AJAX)
- 2.5 Prioritise access to local variables
- 2.6 If you access a DOM element several times, store it in a local variable
- 2.7 Group and minify readings and writings of the DOM and CSSOM
- 2.8 Use the requestAnimationFrame(callback) function for animations and effects which are scroll-dependent
- 2.9 If there are many similar events, group them
- 2.10 Careful with events getting triggered several times in a row
- 2.11 Avoid execution of code chains with eval(), Function(), setTimeOut() and setInterval()
- 2.12 Implement optimisations you would apply in any other programming language
- 3 Tools for detecting problems
- 4 Final recommendations
- 5 Other interesting reads:
With users moving from desktop to mobile devices, client-side optimisation of websites has become increasingly important, to the point of being just as important as server-side optimisation. Getting a website to work fast on devices that are generally slower and highly dependent on an –oftentimes– wobbly network signal, which at times is plain bad or suffers from total outage, is not easy. It is, however, necessary, because having a speedier website will allow us to have more satisfied users who will visit us more frequently, and better rankings for mobile search.
- Response: Interface response in less than 100ms.
- Animation frames each 16ms, which are 60 FPS or 60 images per second.
- Idle time: when a user doesn’t interact with the page, work performed in the background should take 50ms or less.
- Load: the full page must be loaded in 1000ms.
These are the times we must strive to achieve in worst-case scenarios (when the website is executed on an old phone), with little memory and processing resources.
In this post I am going to explain 1) actions that are necessary to optimise download and compilation, and 2) actions that are necessary to optimise the code execution, which is a more technical part, albeit no less important.
Compressing with Brotli q11 and Gzip
The new compression algorithm, Brotli, improves compression by 17%, as opposed to Gzip. If the Internet browser sends the “br” value within the HTTP header’s accept-encoding parameter it will mean the server can send the file in Brotli format instead of Gzip.
This consists in using an automatic tool to remove comments, spaces, and tabs, and to replace variables, making the code much lighter. Minified files must be cached in the server, or be generated already in their minimised form when uploaded, because if the server has to minimise them in each request, it will negatively affect the performance.
Using a tool like webpack we can unify and minimise these two file groups in our development project. Make sure the tool you use to do this generates the so-called “source maps“. These are .map files referred to in the final file’s header, and where the relationship between the files for minified code, unified code and the real source code is established. This way, we will be able to purify our code hassle-free.
<script async src="/code.js" />
This way, we will prevent the script tag from blocking the DOM building stage on our page.
When we use the script tag to inline code on a page, we’re also blocking the DOM build, which gets even worse if we use the document.write() function. In other words, doing this is strictly forbidden:
Here we aren’t only reducing transmission time, but also the time it takes for a browser to analyse and compile the code. To do this, we must keep in mind the following points:
- It’s also possible we might have included by mistake a library that is unnecessary, or that we have libraries, the features of which are already natively available in all browsers, and there is no need to use additional code for it to work faster.
- Finally, if we want to optimise it to the extreme, regardless of the time it may take, we must remove from our libraries all the code we’re not using. I don’t recommend this, though, because we never know when it might become useful again.
Avoid using too much memory
Prevent memory leaks
If we have a memory leak in a loop, the page will increasingly reserve more and more memory, using up all the available memory on the device. This will make everything go much slower. It’s a common mistake in image sliders and carrousels.
Chrome allows us to analyse whether our site has memory leaks, with a timeline recording, located in the “Performance” tab of its developer tools:
Use web workers when you need to execute code requiring a lot of execution time
Use the Fetch API (AJAX)
Prioritise access to local variables
If you access a DOM element several times, store it in a local variable
Group and minify readings and writings of the DOM and CSSOM
When an Internet browser renders a page, it follows a critical path representation, which has the following steps during the first load:
- HTML is received.
- DOM starts to build.
- While DOM is being built, external resources (CSS and JS) are requested.
- CCSOM is built (combination of DOM and CSS).
- Representation tree is created (CSSOM parts to be painted).
- Based on the representation tree, the geometry of each visible part of the tree on a layer is calculated. This stage is called layout or reflow.
- During the final painting stage all the layers from step 6 are processed and get composed one on top of the other, to display the page to the user.
Although browsers enqueue representation tree changes and decide when the repainting should take place, if we have a loop during which we read the DOM and/or CSSOM, and we modify it in the following line, it is possible that the browser will be forced to run reflow or to repaint the page several times, especially if the next reading depends on the previous writing. For that reason, it is recommended:
- To separate all readings to an independent loop and to make all writings at once with the cssText property in CSSOM or innerHTML in DOM. This way the browser will only have to call repaint once.
- If readings depend on previous writings, find a way of rewriting the algorithm to change this.
- If you have no other choice than to apply many changes to a DOM element, take it out of DOM, make the changes, and put it back where it was.
- In Google Chrome we can analyse what happens in the critical path representation with the Lighthouse tool, contained within the “Audits” or “Performance” tabs, which records everything that happens during page loading.
Use the requestAnimationFrame(callback) function for animations and effects which are scroll-dependent
The requestAnimationFrame() function helps the function passed as a parameter to prevent a repaint, until the next scheduled one. Besides avoiding unnecessary repaints, it pauses animations whilst the user is browsing a different tab, saving CPU and battery life.
Effects depending on scroll are slower, because the following DOM properties force a reflow (read step 7 from the previous point) when they are accessed:
offsetTop, offsetLeft, offsetWidth, offsetHeight scrollTop, scrollLeft, scrollWidth, scrollHeight clientTop, clientLeft, clientWidth, clientHeight getComputedStyle() (currentStyle en IE)
If besides accessing one of these properties, we paint a banner or menu based on them, which follow you with a scroll or a parallax scroll effect, several layers will be repainted each time we use the scroll, negatively affecting interface response time, possibly resulting in a jumpy scroll instead of a smooth one. For that reason, with these effects, we must store in a global variable the latest position of the scroll, in the onscroll event. Then we use the requestAnimationFrame() function, only if the previous animation has finished running.
If there are many similar events, group them
If you have many buttons, which, when clicked, perform the same action, we can assign an event to the parent element of all 300 buttons, instead of assigning 300 events to each. When a button is clicked, the event “bubbles” up to its parent, and from there we will know which button the user tapped, modifying the behaviour correspondingly.
Careful with events getting triggered several times in a row
Events like onmousemove or onscroll get triggered several times in a row when an action is performed. Make sure to control the associated code, so that it doesn’t run more times than it is necessary, as this is a rather frequent mistake.
Avoid execution of code chains with eval(), Function(), setTimeOut() and setInterval()
Entering code into a literal to be analysed and compiled during the execution of the remaining code is a rather slow process, for instance: eval(c = a + b);. We can always reprogram this to avoid it.
Implement optimisations you would apply in any other programming language
- Always use algorithms computationally or cyclomatically less demanding for the task in question.
- Use optimal data structures to achieve the previous point.
- Rewrite the algorithm to obtain the same result with less calculations.
- Avoid recursive calls changing the algorithm for an equivalent one which uses a stack.
- Make high-costing functions and repeating calls throughout different blocks of code store the result for the next call in memory.
- Store calculations and calls to repeated functions in variables.
- When going through a loop, store the size of the loop into a variable first, to avoid calculating it again once it’s over in each iteration.
- Factor and simplify mathematical formulas.
- Replace calculations not depending on variables by constants, and leave the calculation commented.
- Use search arrays: they are handy for obtaining a value based on another one, instead of using a switch block.
- Make conditions to be most likely true in all cases, to take better advantage of the speculative execution, as this way the jump prediction will fail less.
- Simply boolean expressions with boolean logic rules, or even better, with Karnaugh maps.
- Use bit operators whenever you can to replace certain operations, as said operators use less processing cycles. Using them requires being familiar with binary arithmetic, for example: with x being the value of a full variable, we can write “y=x>>1;” instead of “y=x/2;” or “y=x&0xFF;” instead of “y=x%256;”.
Tools for detecting problems
We’ve already seen a few throughout this post. Out of all these tools, Lighthouse is the easiest to interpret, as it simply gives us a series of points to improve, just as the ones provided by Google PageSpeed Insights or many others, like GTmetrix. In Chrome we can also use –through the “More Tools” option in the main menu– the task manager, to see the memory and CPU used by each tab. For more technical analyses, we have the dev tools in Firefox and Chrome, where there’s the “Performance” tab, allowing us to analyse more thoroughly the times in each phase, memory leaks, etc. Here’s an example:
Performance is not a requirement that should ever go before ease of error detection and adding functionality.
Other interesting reads:
- CSS optimisation – Ramón Saquete