Frontend Scrapbook

Notes that make a difference

Web Performance

By admin

on Wed Sep 09 2020

Akamai found that 2 second delay in web page load time increase bounce rates by 103 percent

A 400 ms imporvement in performance resulted in a 9% traffic at Yahoo!

100ms improvement in performance results in 1% increase in overall revenue at Amazon

53% of users will leave a mobile site if it takes more than 3 seconds to load.

According to research, if you want user to feel like your website is faster than your competitors, you need to be 20% faster

RAIL: Some numbers ( benchmarks ) to think about. But do not obsess over it!

For web apps, a Response of 0-100 ms, will give instant perception-reaction to users.

100-300 ms, there is a slight perceptible delay

Animation, each frame completes in less than 16 ms. Work should be completed in less than 10 ms, as there is housekeeping work that needs to be done by the browser.

Idle, use the idle time to proactively schedule work. Complete that work in 50 ms chunks.

Load, satisfy the ‘response’ goals during full load. Get the first meaningful paint in 1000 ms.

So, how do we do it ?. Measure before and after. Only do this if you find a performance problem that needs solving.

Doing less stuff takes less time

Javascript Performance: Write code that runs faster, later, or not at all.

Rendering Performance: Most of our javascript happens in the browser, which has its own performance concerns.

Load Performance: Until the user actually gets the page, there isn’t much to optimize. Get things faster to the browser.

Cost of Javascript Performance

Sometimes, parsing and compiling is the real culprit

Javascript is a compiled language. Most browsers use something called JIT (just-in-time) compilation.

Chrome has an engine called V8. Safari has ‘Javascript Core’. Firefox has SpiderMonkey. IE has Chakra.

The JS source code goes through Parser and generates AST. Then it goes through a baseline compiler/interpreter which generates bytecode and runs in the browser. There is an optimizing compiler that looks at any code which can be optimized and generates highly optimized machine code. Some code if cannot be optimized will go back to bytecode.

One way to reduce parsing time is to ship less code. Another way is to parse later ( do the bare minimum now ). The browser does this internally.

Parsing happens in two phases. Eager ( full parse ) and Lazy ( pre-parse ).

Scan through the top-level scope. Parse all the code you see that’s actually doing something. Skip things like function declarations and classes for now. Parse later.

// These will be eager parsed!
const a = 1;
const b = 2;
// function declared will be parsed when needed
function add(a,b) {
 return a+b;
}

add(a,b); // Go back and parse add()!

The above code results in parsing twice!.

If we wrap the function in parenthesis, it will eager-parse!

(function add(a,b){
 return a+b;
})

It doesn’t mean we need to wrap all functions, such optimizations are called micro-optimizations!. Most of the time browser algorithms handle this internally as best fit. We can use the optimize-js plugin if we find performance gains using it. Remember to test before and after.

Try to avoid nested functions!

function sumOfSquares(x,y) {
//function 'square' will be repeatedly parsed lazily.
 function square(n) {
  return n*n;
 }
 return square(x) + square(y);
}

Solution:
function square(n) {
 return n*n;
}
function sumOfSquares(x,y) {
 return square(x) + square(y);
}

In Node.js, we can measure performance using

const performance = require('perf_hooks'); // available in Node.js
performance.mark('start');
....
....
performance.mark('end');
performance.measure('My benchmark','start','stop');
const [measure] = performance.getEntriesByName('My benchmark');
console.log(measure);

node --trace-opt --trace-deopt benchmark.js
const a=10, b= 15, iterations = 100;
while(iterations--){
 add(a,b);
}
add('foo','bar'); // can cause compiler to deoptimize as we give strings which can cause increase in execution time.

We could also use,

%NeverOptimizeFunction(add);

node --allow-natives-syntax benchmark.js , you can see the duration increases and impact performance.

The optimizing compiler optimizes what it’s seen. If it sees something new, that’s problematic. Like in the previous case where it saw add being called with number types multiple times and then later point sees string being passed. So, introducing flow/typescript will help you get potential performance gains!

It turns out there is a type system in place in V8. Browser is good at doing performance optimizations for monomorphic ( eg. similar objects passed to functions )

Browser figures out types of something are internally ( or are you of types same ? ). Most of the engines has some way of keep tracking types. For example, in V8 it is called ‘hidden classes’ or Map.

//index.js
const a = {a: 1};
const b = {b: 1};
console.log(%HaveSameMap(a,b)); // checks keys and type of values.

node --allow-natives-syntax index.js

If the objects end up same, but doesn't go through same path in object creation, V8 doesn't consider them as same objects. It has to start and go through same path.

Scoping and Prototypes have performance implications.

const makeAPoint = () => {
 class Point {
  constructor(x,y) {
   this.x = x;
   this.y = y;
  }
 }
 return new Point(1,2);
}

const a = makeAPoint();
const b = makeAPoint();

console.log(%HaveSameMap(a,b)); // return false.

Note: Each time the function is called it has to define class and it's prototype chain which makes both object different and also slower!

Browser engines do function inlining to increase the performance of the javascript code.

function square(n) {
 return n*n;
}
function sumOfSquares(x,y) {
 return square(x) + square(y);
}
node --trace-turbo-inlining benchmark.js
// function is inlined by the engine.

Use the User Timing API (performance.mark and performance.measure) to figure out the biggest amount of hurt is.

Rendering Performance

DOM, CSSOM are put together to generate ‘Render Tree’. It has a one-to-one mapping with the visible objects on the page. ( no hidden objects, but includes pseudo-elements )

Javascript can come at any point in time and change elements on the page impacting the rendering pipeline.

Style Calculations:

The browser figures out all the styles that will be applied to a given element – is called Style Calculation. The more complicated selectors you have in your CSS, the more complicated you get, the longer this takes.

Class names are simpler, but the nth-child is not.

Consider using simple classes, and reduce the effected elements ( like nth-child ). Use BEM or other systems for example. Also, don’t ship classes you don’t use on the page, because doing less work improves performance!

Layout (a.k.a Reflow ):

Look at the elements and figure out where they go on the page. Because one element can affect other elements on the page. It is a complicated process.

Reflows are very expensive in terms of performance and are one of the main causes of slow DOM scripts. In many cases, they are equivalent to laying out the entire page. Reflow is a blocking operation. It consumes a decent amount of CPU. It is noticeable by the user.

A reflow of an element causes a reflow of its parents and children. It is true in most cases. Even calculating the size or position of an element causes Reflow!

Reflow is followed by a repaint, which is also expensive.

To avoid reflows,

Change classes at the lowest levels of the DOM tree.

Avoid repeatedly modifying inline styles

Trade smoothness for speed when doing animation.

Avoid Table Layouts. A change in a cell can affect all cells around it and elements around it.

Batch DOM manipulation. ( Most of the framework does it with a cost! )

Debounce window resize events.

const button = document.getElementById('double-sizes');
const boxes = Array.from(document.querySelectorAll('.box');
const doubleWidth = (element) => {
  const width = element.offsetWidth;
  element.style.width = `${width*2}px`;
};

button.addEventListener('click', (event) => {
boxes.forEach(doubleWidth);
});


// Optimized solution

button.addEventListener('click' (event) => {
 // FIND WIDTHS FIRST!
 const widths = boxes.map(box => box.offsetWidth);
 // UPDATE ELEMENT STYLE!
 boxes.forEach((element, index, arr) => {
   element.style.width = `${widths[i]*2}px`;
});

Note: Avoid repeating the 'checking' and then 'updating' the elements. It can cause layout trashing or forced synchronous layouts as in above example.

Layout Thrashing occurs when Javascript violently writes, then reads, from the DOM, multiple times causing document reflows.

firstElm.classList.toggle('bigger'); //change
const firstWidth = firstElm.width; //calculate ( cause style calculation and reflow to get most updated information )
secondElm.classList.toggle('bigger'); //change
const secondElmWidth = secondElm.width //calculate

Instead, the below code will cause ‘batch’ and reflow happens only once!

firstElm.classList.toggle('bigger'); //change
secondElm.classList.toggle('bigger'); //change
const firstWidth = firstElm.width; //calculate
const secondElmWidth = secondElm.width //calculate

We could also try, requestAnimationFrame ( async ), so that you could read all the values first and write it inside a request animation frame. It asks the browser to delay render to the next available start of frame to render.

const button = document.getElementById('double-sizes');
const boxes = Array.from(document.querySelectorAll('.box');
const doubleWidth = (element) => {
  const width = element.offsetWidth;
  requestAnimationFrame(() => {
   element.style.width = `${width*2}px`;
  });
};

button.addEventListener('click', (event) => {
boxes.forEach(doubleWidth);
});

FASTDOM

A library that can help layout trashing by batching reads and writes.

const button = document.getElementById('double-sizes');
const boxes = Array.from(document.querySelectorAll('.box');
const doubleWidth = (element) => {
 fastdom.measure(() => {
  const width = element.offsetWidth;
  fastdom.mutate(() => {
   element.style.width = `${width*2}px`;
  });
 });
};

button.addEventListener('click', (event) => {
  boxes.forEach(doubleWidth);
});

Paint

Draw pixels to the screen.

Painting is the process of filling in pixels. It involves drawing out text, colors, images, borders, and shadows, essentially every visual part of the elements. The drawing is typically done onto multiple surfaces, often called layers.

Changing some properties like background, color, opacity, CSS transform won’t cause layouts and reflows for example. Sometimes we can skip steps on the rendering pipelining and do less.

Anytime you change something other than opacity or a CSS transform, you are going to trigger a paint.

Triggering a layout will always trigger paint.

Compositor Thread

The UI thread: Chrome itself. The tab bar etc.

The Renderer Thread: We usually call this the main thread. One per tab. This is where all the Javascript, parsing HTML and CSS, style calculation, layout, and painting happens.

The Compositor Thread: Draws bitmaps to the screen via the GPU.

When we paint, we create bitmaps for the elements, put them onto layers, and prepare shaders for animations if necessary. After painting, the bitmaps are shared with a thread on the GPU to do the actual compositing. The GPU process works with OpenGL to make magic happen on your screen.

The main thread is CPU-intensive and the compositor thread is GPU-intensive.

The main thread has way more responsibilities usually. If we can offload some responsibilities to GPU ( less-busy ), then we should do that. We can do it by managing layers.

Things usually compositor thread is usually good at:

Drawing the same bitmaps over and over in different places.

Scaling and rotating bitmaps.

Making bitmaps transparent.

Applying filters.

Mining bitcoin!

Compositing manually is usually a kind of hack. It’s not in the W3C spec. Layers are usually handled by the browser. But we could make suggestions to the browser.

What kind of stuff gets its own layer :

The root object of the page.

Objects that have specific CSS positions ( like position ‘fixed’ )

Objects with CSS transforms

Objects that have overflow , video, canvas, SVG … etc.

We could make suggestions with ‘will-change‘. will-change is for things that will change. ( not things that are changing ). By promoting elements to GPU and using CSS3 transforms we could avoid Paint storming.

.sidebar {
 will-change: transform //IE and Edge doesn't support!
 transform: translateZ(0); // FORCE to make own layer. a hack.
}

//A terrible idea.
* {
 will-change: transform;
}

//Not useful
.sidebar.is-opening {
  will-change: transform;
  transition: transform 0.5s;
  transform: translate(400px);
}

//Useful

.sidebar {
 transition: transform 0.5s;
}
.sidebar:hover {
 will-change: transform; // because promoting object will take non-zero amount of time. if user hovers, it is likely that he/she will open. so it will have smoother transform
}
.sidebar.open {
 transform: translate(400px);
}

will-change through Javascript:

If it’s something that the user is interacting with constantly, add it to the CSS. otherwise, do it with javascript!

Through JS, add will change, do the thing, and put it back when it is not going to change anymore.

element.addEventListener('mouseenter', () => {
 element.style.willChange = 'transform';
});

element.addEventListener('animationEnd', () => {
 element.style.willChange = 'auto';
});

element.addEventListener('mouseleave', () => {
 element.style.willChange = 'auto';
});

Load Performance:

Bandwidth vs Latency

Bandwidth is how much stuff you can fit through the tube per second. Latency is how long does it take to the other end of the tube.

TCP focus on reliability.

TCP starts by sending a small amount of data and then starts sending more and more as we find out that things are successful.

The initial window size is 14kb. So, if you can get files under 14kb, then it means you can get everything through the first window.

HTTP/1.1 added the Cache-Control response header.

Caching only affects the ‘safe’ HTTP methods like GET, OPTIONS and HEAD

Cache-Control: no-store // The browser gets a new version every time

Cache-Control: no-cache // This means you can store a copy, but you can’t use it without checking with the server

Cache-Control: max-age // use until expiry. don’t bother to check with the server.

Content Addressable Storage: checksum of the file is added to the file name. every time we build, it generates a new checksum.

Caching for CDNs

CDNs respects the max-age header just like the browsers. We want CSS, JS to be cached by the browser. We would like the CDN to cache the HTML that it serves up. But we don’t want the browser to. ( because the JS and CSS file names need to be generated dynamically at every new build and referred through HTML )

Cache-Control: s-maxage=3600 // in seconds. this is for CDNs only. Tell the CDN to keep it forever. But don’t tell the browser to do it.

Service workers are another thing.

Lazy-loading and pre-loading with React and Webpack. We could use react-loadable plugin or React.lazy ( available in newer versions )

import Loadable from 'react-loadable';
import Loading from './Loading';
const LoadableEditor = Loadable({
 loader: () => import('./Editor');
 loading: Loading
});

export default LoadableEditor

Analyze webpack bundles with webpack bundle analyzer.

HTTP/2

Fully multiplexed – send multiple requests in parallel. HTTP/1 has no pipelining whereas HTTP/2 does.

Allows the server to proactively push responses into client caches.

Bandwidth has gotten a lot better, but roundtrip time hasn’t. It takes just as long to ping a server now as it did 20 years ago.

Build Tools

purifyCSS – strips out all unused CSS.

Babel

Babel allows us to write future JS and compiles down to javascript that works in older browsers. Configure Babel as required and avoid paying the babel tax as far as possible ( reduce generated code size )

Some useful React babel plugins :

@babel/plugin-transform-react-remove-props-types

babel-plugin-transform-react-pure-class-to-function

@babel/plugin-transform-react-inline-elements

@babel/plugin-transform-react-constant-elements

Some other areas of performance improvements are :

Server-side rendering,

Image performance,

Loading web fonts,

Progressive web applications