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.
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/.
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:
AppComponentwe have an observable
article$which will hold the article object to be displayed.
ngOnInitwe assign to the return value of the
getArticle()method to the property
getArticle()simulates an API call to fetch the article data from the server. It returns a mocked
IArticleobject 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.
- We define
IArticleinterface, with the properties title, body, and imageUrl.
- In the template, we subscribe to the
article$using the async pipe, once resolved we display the data from the returned article object. It is important to specify the
heightproperty of the image. Otherwise, it will have a very big negative impact on the CLS value.
- We display a loading indicator while the article is still loading.
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.
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:
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.
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
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 Angular official documentation.
At this point our application supports server-sider rendering, and you can double check this by serving the application using:
npm run dev:ssr
open the browser and navigate to the application url, you will now notice that article content is being displayed immediately.
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:
- Angular Universal will render the application on the server-side and send the final HTML to the client.
- The client receives the rendered HTML and displays it immediately to the user.
If we tried to measure the CLS at this time, the result will be event worse than before. First we build our application:
npm run build:ssr
Then lets serve it:
npm run serve: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.
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
TransferState, and by definition TransferState is:
A key value store that is transferred from the application on the server side to the application on the client side.Angular Official Documentation
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:
- We imported
AppServerModulesince the TransferState service is provided within these modules.
- We check if the application is running on the server-side
if(isPlatformServer(this.platformId)), we load the article from the API, and we store it in the transfer state
- If the application is running on the client-side, we check if the
transferStatehas a value from the server
- If the
transferStatehas a value, we use it directly, otherwise, we reload the article from the API.
And thats it, lets build and serve our application again, and 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.