DEV Community

Cover image for User Experience and Core Web Vitals Optimization – Angular Universal.
Nichola Alkhouri for This is Angular

Posted on • Edited on

User Experience and Core Web Vitals Optimization – Angular Universal.

This article was originally published here

In this article, I will use Angular Universal to server-side render a sample web application. This would help to improve the user experience and will boost Core Web Vitals scores. At the same time, I will show you that just enabling server-side rendering, without taking any further steps, can negatively impact those Core Web Vitals, and especially CLS.


Core Web Vitals and CLS To Measure User Experience

Before we start, if you want to know more about Core Web Vitals, you can visit this page https://web.dev/vitals/. I will provide a brief definition as they appeared in that article.

Web Vitals is an initiative by Google to provide unified guidance for quality signals that are essential to delivering a great user experience on the web. Philip Walton

Core Web Vitals are the subset of Web Vitals that apply to all web pages, should be measured by all site owners, and will be surfaced across all Google tools. Each of the Core Web Vitals represents a distinct facet of the user experience, is measurable in the field, and reflects the real-world experience of a critical user-centric outcome.

Philip Walton

From above, Core Web Vitals help us to measure and optimize the User Experience of our web application. I will focus in this article on one specific signal of those Vitals, the CLS (Cumulative Layout Shift).

CLS measures the "Visual Stability" of the web application, It reflects how stable your page is, and is affected by the sudden movement or the unexpected changes of the content which happen while the user is reading through. The ideal value of CLS is below "0.1". You can find more about CLS in this article https://web.dev/cls/.


Simple Application Without SSR

Let's first create a simple Angular client-side rendered application, our application will have only one functionality, it will fetch an Article from the server, and then display the articles in the view. We create the application by running:

ng new cls-measuer-app
```


Now lets update the app.component.ts, app.component.html to be as the following:

<ul><li>In <code>AppComponent</code> we have an observable <code>article$</code> which will hold the article object to be displayed.</li><li>In <code>ngOnInit</code> we assign to the return value of the <code>getArticle()</code> method to the property <code>article$</code>.</li><li><code>getArticle()</code> simulates an API call to fetch the article data from the server. It returns a mocked <code>IArticle</code> object from a json file with a delay of 500 ms. Notice here that the content of the article should be big enough to fill the entire page. This is essential to simulate a real-life example of an article.</li><li>We define <code>IArticle</code> interface, with the properties title, body, and imageUrl.</li><li>In the template, we subscribe to the <code>article$</code> using the async pipe, once resolved we display the data from the returned article object. It is important to specify the <code>height</code> property of the image. Otherwise, it will have a very big negative impact on the CLS value.</li><li>We display a loading indicator while the article is still loading.</li></ul>

<p>Once we serve and browse the application we will see the header, the footer, and a loading indicator. After half a second the article content would have been loaded and the loading indicator will be replaced with the article content.</p>

Measure Core Web Vitals

To measure the CLS, I am using Web Vitals Chrome extension. I will run the application in production mode and trigger the measuring which will give me the following results:

measures-initial

Notice the value of CLS (0.122) which is higher than the ideal value (0.1). This means that we are not providing a good user experience in our application. The reason behind this is that we are replacing the loading indicator with the article content. This is considered an unexpected change of content and add a negative impact on the CLS signal.

Luckily this problem is fixable in Angular applications. The solution is to avoid this shifting in the content by providing the final state of the page (after the article is loaded) as fast as possible to the user. We can do so by rendering the page on the server-side using Angular Universal.

<hr/>

Server-side rendering (SSR) with Angular Universal to Improve User Experience

Now we will start solving the above problem. We start by adding Angular Universal to our application to allow server-side rendering of the initial page load. So let's run the following command:

ng add @nguniversal/express-engine
Enter fullscreen mode Exit fullscreen mode

<p>This will prepare the project, installs universal/express-engine, and creates the server-side app module and many other files, you can find more about this command and the files it generates in the <a href="https://angular.io/guide/universal"&gt;Angular official documentation</a>.</p>

<p>At this point our application supports server-sider rendering, and you can double check this by serving the application using:</p>

npm run dev:ssr
Enter fullscreen mode Exit fullscreen mode

open the browser and navigate to the application url, you will now notice that article content is being displayed immediately.

That Is Not Enough

<p>Now our application is server-side rendered and is providing the user with the content of the page in no time. However, we still have a problem here. When you run your application, even though you see the Article content immediately, you will notice after a small period of time that the loading indicator will flicker for a moment, and the article content fills the page again. This is a result of the following sequence of execution:</p>

<ol><li>Angular Universal will render the application on the server-side and send the final HTML to the client.</li><li>The client receives the rendered HTML and displays it immediately to the user.</li><li>The browser loaded all javascript files and re-hydrated our application. After that, the application triggers the article loading process and displays the loading indicator and replaced the indicator with the article content when the loading is finish.</li></ol>

<p>If we tried to measure the CLS at this time, the result will be event worse than before. First we build our application:</p>

npm run build:ssr
Enter fullscreen mode Exit fullscreen mode

Then lets serve it:

npm run serve:ssr
Enter fullscreen mode Exit fullscreen mode

Now we run the application and takes the measurements again, and her is the result:
measures-after-ssr

Notice that CLS is now (0.168) which is higher than the previous value before enabling SSR. In other words, by enabling SSR we made things even worse than before, fortunately, there is a solution for this problem.

TransferState to the rescue

<p>The main reason for the problem we are facing is that our application is loading the article twice, once on the server-side, and then again on the client-side after the browser re-hydrated the application. To fix this we can use <code>TransferState</code>, and by definition TransferState is:</p>

<blockquote class="wp-block-quote"><p>A key value store that is transferred from the application on the server side to the application on the client side.</p><cite><a href="https://angular.io/api/platform-browser/TransferState"&gt;Angular Official Documentation</a></cite></blockquote>

server fetched from the API, and pass it to the client-side application, so we don't have to call the server again on the client-side, to do so we implement the following changes:</p>

<ol><li>We imported <code>BrowserTransferStateModule</code> in the <code>AppModule</code>, and <code>ServerTransferStateModule</code> in <code>AppServerModule</code> since the TransferState service is provided within these modules.</li><li>We check if the application is running on the server-side <code>if(isPlatformServer(this.platformId))</code>, we load the article from the API, and we store it in the transfer state <code>this.transferState.set(this.stateKey, article)</code>.</li><li>If the application is running on the client-side, we check if the <code>transferState</code> has a value from the server <code>this.transferState.hasKey(this.stateKey)</code>.</li><li>If the <code>transferState</code> has a value, we use it directly, otherwise, we reload the article from the API.</li></ol>

<p>And thats it, lets build and serve our application again, and measure:</p>

metrict-last-measure

And now CLS is down to 0, and that is because the content of the page is never change after the initial rendering.

Overall, if you want to boost your SEO, don't just enable Angular Universal in your application and assume that server-rendering your pages will solve all your SEO problems. Enabling Angular Universal is only the first step. You need to take further steps to make sure that your application is working as intended. and don't forget to measure first then change and compare.

Enter fullscreen mode Exit fullscreen mode

Top comments (0)