This article was originally published here

Server-side rendering a sample web application with Angular Universal is the goal here — it improves the user experience and lifts Core Web Vitals scores. But there is a catch worth demonstrating along the way: switching on SSR by itself, with no additional work, can drag those same Core Web Vitals down, CLS in particular.


Using Core Web Vitals and CLS to Gauge User Experience

If Core Web Vitals is unfamiliar territory, the page at https://web.dev/vitals/ is a good starting point. The short definition below is taken from 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

What follows from that is simple: Core Web Vitals give us a way to quantify and then improve the User Experience of a web application. Of all those signals, this article concentrates on a single one — CLS, or Cumulative Layout Shift.

CLS is the metric behind "Visual Stability", telling you how steady a page remains. Content that jumps around or changes unexpectedly while somebody is reading is what pushes the score in the wrong direction. Staying under "0.1" is considered ideal for CLS. Further detail on CLS lives in this article: https://web.dev/cls/.


A Basic Application, Client-Side Rendered

Start with a plain Angular application rendered on the client. It does exactly one thing: request an Article from the server and render that article in the view. The scaffold comes from:

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