Website Performance Diagnosis and possible improvements

Arpit kumar
heycar
Published in
15 min readAug 17, 2021

Performance of a website has become an integral part of the website development these days. It directly affects the end users landing on the website which leads to a positive/negative impact on conversion. It’s one of the contributing factor towards feature success and business growth.

In layman language, If I land on the website of an ‘XYZ’ product, I would like to see the page instantly considering that my network speed is exceptional. It will encourage me to browse the website more and follow the tunnel.

User Impression for a slow website — ☹️

User Impression for a fast website — 😃

Optimisation for quality of user experience could be the key to success of
any website. Here comes the Web Vitals, basically the website performance metrics which help us to know the quality and speed of our website. It can help us to find the opportunities to improve the experience and quality of the website.

Core Web Vitals (CWV) is an initiative from Google to provide a unified guidance to deliver a great user experience on the web. There are majorly three aspects of the user experience — loading, interactivity, and visual stability — and includes the following metrics :-

  • FCP (First Contentful Paint)
  • LCP (Largest Contentful Paint)
  • CLS (Cumulative Layout Shift)
  • FID (First Input Delay)

Many companies have gained customers visits to their website by improving above factors. Below are some examples :-

  • AliExpress improved CLS by 10 times and LCP by double, which translated to 15% lesser bounce rates.
  • Flipkart achieved 2.6% reduction in bounce rate by improving Core Web Vitals metrics.
  • NIKKEI STYLE’s 18% LCP improvement resulted in 9% more pageviews per session.
  • Yahoo! Japan fixed CLS which led to a 98% reduction in poor pages and 15% uplift in page views per session.
  • And many more…..

First we start with finding out the web vitals score for the website. Google offers several tools to measure and diagnose the website performance.

For the developer understanding, it’s good to check out the chrome dev tools which help us to deep dive in the root cause of poor score of individual metrics.

At heycar, along with pagespeed insights and webdev , we went through Chrome Dev Tools to see more insights on how the web browsers see the page in terms of Core Web Vitals. There were some great learnings in debugging. We tried different google suggested solutions (mentioned below in this article) and simultaneously checked the pros/cons of them in the local development/staging environment.

Below are the demonstration of using Chrome Dev Tool to visualise CWV in detail and the solutions which we tried at heycar.

FCP (First Contentful Paint)

First paint is basically a first impression of the website.
FCP is the time when first text/image is visible on the page. An ideal FCP is anything less than or equal to 2 seconds.

Steps to check FCP of a website in chrome devtools :-

  • Open the website in chrome
  • Open devtools and select performance tab
  • Start the profiling to see various parameters such as Network, Timings, animation, main thread, etc.
  • After the profiling is finished , expand the Timing section and look for FCP placeholder and note the time.

So If we are looking for options to fix FCP , then we can debug in a way as shown above locally.

We can also get the score programatically with web-vitals Js library. Below snippet can be put inside head tag of html to check the FCP score.

<script type="module">
import { getFCP } from 'https://unpkg.com/webvitals?module';
getFCP(console.log);
</script>

LCP (Largest Contentful Paint)

This is the metric which gives information about time taken for largest text/image/video/block-level elements with text nodes to appear above the fold i.e. on the first viewport. An Ideal LCP is anything less than 2.5sec.

Steps to check LCP of a website in chrome devtool :-

  • Please follow all the first 3 steps as for FCP.
  • Expand the Timings section to see the LCP placeholder. The placeholder when clicked/hovered , we can see the LCP element on the page and its time.
<script type="module">
import {getLCP} from 'https://unpkg.com/web-vitals?module';
getLCP(console.log);
</script>

We can get the score programatically with web-vitals library similarly. Above snippet can be put inside head tag of html to get the LCP score. The output on the console will show us the LCP element in ‘entries’ property inside the object.

Some suggested solutions over the web for improving LCP and FCP are as follows: —

Eliminate render blocking resources — Normally the third party scripts and large Js files in the head tag of the html are the root cause of blocking the page render. The loading of some of the third party scripts can be delayed or can be loaded as soon as user starts the interaction (page scroll, mouse click, etc). Usage of async and defer attribute in the script can delay the parsing, compilation and execution of script after the html layout. Sending critical resources i.e. send JS code for what is needed in the beginning.

Minification and uglification of css, javascript and html

Preconnect/Dns-prefetch :- It helps browser in establishing an early connection to the external domains like image CDNs, API endpoints, etc.

Serving static assets with an efficient cache policy :- Google suggests to cache the static assets for 1 year with cache busting in place i.e. appending the hash after filename through build tool.

Avoid an excessive DOM size :- Lighthouse complains for a webpage if it has more than 1500 nodes, having depth greater than 32 nodes and having a parent node with more than 60 child nodes. This can be tested in chrome devtools with subtree modification. More info: — https://web.dev/dom-size/

Reducing server response time (TTFB) :- This could be more in SSR application as the logics, calculations and build up happens on the server side. So we should avoid as much as to delay the execution/loading of some Js code on client side which are not required for SEO crawlers.

Preload of scripts, fonts and css :- We can add preload links in our HTML to instruct the browser to download key resources as soon as possible. More info: — https://web.dev/uses-rel-preload/

Differrence between Preload and Prefetch a resource.

Preload is a declarative fetch, allowing you to force the browser to make a request for a resource without blocking the document’s onload event. Preload is destined for current navigation.

Prefetch is a hint to the browser that a resource might be needed, but delegates deciding whether and when loading it is a good idea or not to the browser. It is a directive that tells a browser to fetch a resource that will probably be needed for the next navigation.

Lazyload images below the fold :- We should lazyload the images which are not on the viewport to reduce the initial network requests and transfer size. Intersection observer is a good candidate here to lazyload the images.

There are couple of lazy-loading libraries in the market which ensures the images on the website gets loaded when scrolled. They (for e.g. react-lazyload) basically use scrolling to reach to the element.

Now here comes the problem for some SEO crawlers. Most of the crawlers can’t scroll the page. A very diligent crawler may theoretically perform a scroll-and-wait action multiple times, but this process is costly in time and doesn’t always produce results. Unlike users, scrolling can be complicated for crawlers.

So, Google suggested solutions to handle lazy-loading is through below options:-

Image Optimisation:-

  • Usage of webp format images (lossless compression and same quality in lesser size)
  • Using CDNs to serve images faster, also pre-fetching the cdn link in head tag of html.
  • Use sprite image for collection of small image
  • Avoid the usage of icons as image if the icons are possible to load with font family or css work.
  • Prefer vector images (Svgs ) whenever possible as they are resolution-independent and always deliver sharp results.
  • Use responsive images if raster images (jpg, png, etc) are required.

Success story at Heycar
We have worked on some of the above factors to improve our FCP and LCP.

  • Lazy-loading / Delaying the load of third party scripts like Segment, Google ads and tv tracking pixels.
  • Using dns-prefetch to preconnect to our BE API endpoint, CDN server and image server to establish early connection.
  • All our static assests like images/JS files are cached for 1 year with cache busting.
  • Preload is applied to font files and main Js to download them as early as possible.
  • Using Intersection observer to lazy load most of the images below the fold which saves the initial amount of network requests.

CLS (Cumulative Layout Shift)

It’s basically a shift noticed in the elements on the page during load. Some reasons could be fonts weight change (between original font and system fonts) , dynamic addition of elements which moves other elements on page. Content that shifts unexpectedly on the page creates a bad user experience but when it moves gradually from one position to the next can often help the user better understand what’s going on and guide them between state changes.

Steps to check CLS of a website in chrome devtools :-

  • Follow all the first 3 steps as for FCP
  • Expand the Experience section to see for various Layout shift contributors. The placeholder when clicked/hovered shows the element which was affected.

Below is the screenshot for the same.

There is a specific score for each layout shift, which adds to a total score. It’s calculated between two frames by 2 factors — Impact fraction and distance fraction. Impact fraction is the union of element’s visible area in both frames. Distance fraction is by how much % the element has moved with respect to viewport height.

LS score = Impact fraction (e.g. 0.75) * distance fraction (e.g. 0.25) = 0.1875

We can get the total CLS score of all the contributing elements programatically with web-vitals library similarly.

<script type="module">
import {getCLS} from 'https://unpkg.com/web-vitals?module';
getCLS(console.log);
</script>

Few solutions to improve CLS

  • Use CSS transform for animations instead of changing height, width, top, right, bottom or left to move elements around. For e.g. transform: scale() and transform: translate()
  • Include size attributes (width and height) to images and videos to reserve the space before the user sees the content. It can be done by keeping the placeholder/shims also as can be seen below.
  • Never insert content above existing content, except in response to a user interaction. This ensures any layout shifts that occur are expected.

Fonts loading can play a major role in Layout shifting, mainly for slower network connection.

Fonts have different x-heights. Therefore, our fallback font and web font will take up a different amount of space. One of the best practice in the recent time is to use font-face-observer library

With the above library we get the loading state of custom font family. When it’s loaded, add a custom class on body or html tag. In the meantime i.e. before the custom font gets loaded, we can apply custom css for system fonts (line-height and letter spacing) to the texts and match the spacing with original font (nearest resemblance).

Font face observer used along with browserstorage and preload( the font files) will give better output. More Info :-
https://www.zachleat.com/web/comprehensive-webfonts/

Below could be an example.

const font = new FontFaceObserver('CustomFont');
font.load().then(function () {
document.documentElement.className += " webfont-loaded";
});
/* Before font gets loaded.*/
h1 {
font-family: sans-serif;
letter-spacing: 1px;
line-height: 0.95;
}
/* After font gets loaded.*/.webfont-loaded h1 {
font-family: 'Objektiv', sans-serif;
letter-spacing: 0;
line-height: 1;
}

Success story at Heycar
We have worked on some of the above factors to improve Layout Shifting.

  • Adding width and height properties to images to reserve the space on the browser along with that we also have placeholders/shims.
  • To avoid the text shifting which were impacting much on our CLS score, we applied FontFaceObserver.

Problem:- Since the original font-family takes some seconds to load , so in the meantime the browser shows the text in the fallback fonts provided which has different x-heights/letter spacing. This cause the initial shifting of elements to the end users mostly of slow devices (Fear of Unstyled Texts). On our page the sentence which is supposed to show in 2 lines with the original font-family was showing in 1 line initially with system font.

Solution:- We tried the above font solution and CLS score improved.

FID ( First Input Delay)

This is basically a time difference of user action triggered to browser and browser response for it. Anything lesser than or equal to 100ms is a good score.

This can happen because of multiple factors like third party scripts loads intervention to the parsing, compilation and execution of main thread. There could be several Long task (any task which takes more than 50ms) , unused javascript code , large html document size, etc.

We can visualise the main thread of application and look for various time length responsible for parsing of html and execution of javascript code.

In the performance tab, after profiling , we can expand main section to see tasks and parsing of html duration.

<script type="module">
import { getFID} from 'https://unpkg.com/web-vitals?module';
getFID(console.log);
</script>

Note: For getting FID, the user needs to do an action (any event trigger like clicking button, input element , etc) on the page, scroll event doesn’t count in that.

Ways to improve FID :-

  • Reduce the impact of third-party code

All the third party Javascript files which we need for e.g. Social network integration, analytics and metric script, advertisement iframes, A/B testing scripts for experiment, in short , scripts that can be embedded into any site directly from a third-party vendor play a major role in blocking the main thread of browser. Main thread of the browser is responsible to do document parsing, JS parsing, compilation and execution.

We can improve this with the below solutions

  1. Load the script using the async or defer attribute to avoid blocking document parsing.

Difference between async and defer

For async script tag, it executes just after downloading which can or cannot block the main document parsing.

For defer script tag, it will always run and execute after the main document is done with the parsing.

  1. Consider removing the script if it doesn’t add clear value to your site.
  2. Consider Resource Hints like <link rel=preconnect> or <link rel=dns-prefetch> to perform a DNS lookup for domains hosting third-party scripts.
  3. Lazy loading third party resources. For example, serving an ad in the footer only when a user scrolls down the page. Another pattern is lazy-loading content after the main page content loads but before a user might otherwise interact with the page.
  • Reduce JavaScript execution time

Javascript gets parsed, compiled and executed on the main thread of browser and if the main thread is busy , it will create a lag for users basically increasing the TTI(Time to Interactivity)

Code-splitting is beneficial here, like sending as much code as is responsible in the first go. Dynamically load other modules on demand. For example, if there is a component which renders in a modal and the modal opens on user action, then that component’s import can be done dynamically so that it create a separate chunk out of it.

Removing unused code:- This can be monitored using chrome devtools and a bundleAnalyzer library. We can check the chrome devtools and look for Coverage tab to see the amount of unused css and js. It will help us in deciding which piece of code we can get rid of atleast in the initial bundle.

If we are using webpack , then we can use webpack bundle analyzer to check what’s making up the bundle and find out which library is taking how much and decide on getting rid of the unused ones.

  • Minimize main thread work.

Main thread of the browser basically handles most of the code like parsing HTML, building DOM/CSSOM, parsing CSS and applying specific styles, parsing-compiling-execution of Javascript.

Using web worker can be a major contributor here apart from code-splitting, less DOM size and etc.

Web Worker:-

Web worker is a Browser API which offloads a non-UI logic computation from main thread to a separate worker thread, hence making the main thread free for other task.

Findings about Offloading Main Thread (using webworker)

  • It offers a promising way to increase performance on slow devices without adversely affecting users of high-end devices.
  • We are just moving work from the main thread, not reducing the work.
  • The extra communication overhead between the web worker and the main thread can sometimes make things marginally slower.
  • Not useful in cases when all the computation logic happens on server side for a page but we can use it if some feature is available on just client side and it’s having a heavy non-UI logic. It might come up with introducing extra code and little overhead of the communication between worker thread and main thread.
  • We cannot put third party scripts inside webworkers as some of them has direct relation with DOMs.

An article for usage of Webworker for redux actions/reducers which briefly says updating store in terms of updating a DOM is very less time consuming and hence not that helpful. Imagine a case, If a user clicks a button which fetches some results from api , why would the end user want the page to be responsive in that time frame.

https://david-gilbertson.medium.com/should-you-should-be-using-web-workers-hint-probably-not-9b6d26dc8c6a

  • Keep request counts low and transfer sizes small

We should try to keep the initial network download size as low as possible. Major contribution here comes from Image, Script, Font, Stylesheet, Document, and Media rows.

Image: — Here image optimization can be done i.e. compression without losing quality. Images are actually not render blocking resources but still it affects the load performance.

Scripts and Css: — They are render blocking resources. Compression algorithm should be applied for scripts, css and html to reduce the file size. Gzip and brotli compression reduces the size by huge margin.

Fonts:- Inefficient fonts loading can maximize the Fear of Unstyled Text . So we can follow the font loading technique as decribed above (Font Face Observer)

Document:- It takes more time for the browser to parse the html and make DOM nodes if the document size is too large. Logically, in the SSR application , the document size is too large. We can discard or avoid sending some html which is not needed for SEO crawlers. Postponing the component load at client side could also be an option (Lazyload component).

Success story at Heycar
We have worked on some of the above factors to improve FID.

  • For most of the third party scripts , we are doing the lazyloading i.e. delaying the scripts load. We have used defer attribute for our main JS bundle.
  • We use thumbor image service to optimise our images.
  • For code splitting, we are loading component dynamically/lazily on demand/on action which helps in offloading the main bundle to some extent.
  • We ran bundle-analyzer to see the size occupied by various plugins/libraries. For e.g. Core JS (es6 polyfill) was occupying 45kb approx., we moved it to script nomodule tag which will be served only on non-es6 browsers. <script nomodule src=”core js cdn link”></script>
  • To minimize main thread work, we did a proof of concept for using web-worker. We realised that it will be nice to use it for heavy non-UI computation/business logic on client side which has no direct dependency with html markup. More details explained above on Web-worker.
  • We also offloaded our BE API response object to cater only needed data on FE. This offloaded our document size which had a positive impact on FCP and FID.

Conclusion:

As we saw in the above sections , the ways to diagnose CWV metrics with chrome devtools and web-vitals Js, so it will give more clarity for the developers, the reasons behind poor CWV metrics value. There were several solutions and suggestions discussed for improving it.

We cannot have a perfect score for performance but we could work in the possibilities of improvement to make the website surfing experience smoother for our end-customers.

Resource referred :-

https://web.dev/

--

--