Motivation
The main benefit behind using a frontend framework, such as Angular, React, or another, is that it renders directly on the client's side (browser), providing the user with interactivity and smooth transitions across the platform. Nonetheless, this very benefit has shown some side effects that are not negligible.
Problems of Client-Side Rendering
- The first and biggest issue with client-side rendering is that it puts all the load of data transfer on the user's machine (i.e. the browser), hence increasing the loading time of the data and the pages of the platform relative to the internet connectivity of the user.
- Another problem with rendering the application on the client's side is losing the ability to load dynamic cards when sharing a link to a page in the application on a social media platform. One of the powers of doing so is that most social media platforms are equipped with tools to fetch the metadata of the page associated with the shared link without actually loading the page and rendering a dynamic card representing this metadata. However, since these tools do not load the actual page, they will always fetch the default metadata of the application, rather than the ones specific to the target page, which usually are set after the page loads.
- The third and final problem is similar to the previous one but targets search engines like Google. Like social media platforms, search engines fetch the metadata of the pages, looking for information like title, description, author, keywords, etc, without actually loading the page. So unless the pages are being rendered on the server's side before returning the result to the client, the default metadata will always be returned to the search engines.
Here is an example of an article from this website that was shared on LinkedIn before adding server-side rendering:
You can see that the post card has the default title and image of the website, instead of the ones of the article itself.
What is Server-Side Rendering?
The concept behind rendering an application on the server's side is to load the page on the server and return it fully or partially loaded to the user, so that instead of receiving a blank page on the user's side and then fetching the required data from the server over HTTP to populate the page, the user will receive the page populated and ready for rendering.
This, of course, applies to the first time we open the application in a browser session. In other words, server-side rendering does not apply for the scenarios where we navigate within the application using the routing mechanism offered by the framework we're using, since they usually optimize the routing by reloading the necessary components on the browser instead of refreshing the page. Thus, server-side rendering works whenever we go directly to a specific link inside the application or refresh the page.
How does SSR address the problems mentioned earlier?
When using SSR, the content first returned when accessing a link will be the actual target page already loaded for us, as opposed to a blank page which will then get loaded on the client's side, which is the case with client-side rendering. This way, we save time for the user by preparing the page for them, so they don't have to wait long before they first see some content, and we provide the social media and search engine bots with a functioning page with the needed metadata.
Server-Side Rendering with Angular
In the next section of the article, we will follow an example of how to work with server-side rendering in Angular. Angular-SSR requires Angular of version 17 or above, so make sure that your application meets the requirement.
Angular without Server-Side Rendering
First, we will look at an example of an Angular application without server-side rendering. Here is the repo we will be working with: https://github.com/mohammed-ezzedine/articles-angular-ssr.git. In this first commit, you can see that we have a simple Angular project with a home component that renders under the path “/home”. This component fetches the summary of this article and sets its metadata based on it.
If we run the application at this commit and navigate to the home page, we can see that the title of the tab is the same as this article. However, if we check the Network page, and inspect the request that fetched the home page, we will find that they were not provided like that from the server:
This is what social media and search engine bots will see as well.
Adding Angular-SSR to an existing Angular project
If you're starting a project from scratch, you'll be asked if you want to enable SSR by default. However, if you have an existing project, you will need to do it manually. To do we will need to add the Angular-SSR scheme by running the following command:
ng add @angular/ssr
Once done, you can notice some changes in the project directory. More of that can be found in the Angular-SSR official documentation.
Running the application
Let's now run the application to test the new changes:
npm run build
npm run serve:ssr:client
You'll see that the application started at port 4000. If we navigate to http://localhost:4000/home and check the Network tab, you'll find the following:
Now, the metadata of the page is being returned on the first request
Transferring State
When you request a link from the application, it loads the page, in our case by fetching the needed information from a REST API. However, once the response is received in our browser, we don't want to call the API again and populate the page, since it is already loaded. That's why, we can transfer the results from the server to the client like the following:
export class HomeComponent implements OnInit {
private readonly SERVER_DATA_KEY = makeStateKey<Article>("homeArticle"); // 1
constructor(private http: HttpClient,
private transferState: TransferState, // 2
@Inject(PLATFORM_ID) private platformId: Object // 3
) { }
ngOnInit(): void {
if (isPlatformBrowser(this.platformId)) { // 4
if (this.transferState.hasKey(this.SERVER_DATA_KEY)) { // 5
let article = this.transferState.get<any>(this.SERVER_DATA_KEY, undefined); // 6
this.initializeMetadata(article)
this.transferState.remove(this.SERVER_DATA_KEY); // 7
return
}
}
this.http.get<Article>("https://mohammed.ezzedine.me/api/articles/QK7WwzbwbvX")
.subscribe({
next: data => {
if (isPlatformServer(this.platformId)) { // 8
this.transferState.set(this.SERVER_DATA_KEY, data);
}
this.initializeMetadata(data)
}
})
}
// ...
}
Notice how we are:
- creating an identifier key for the data we want to transfer
- injecting a service to read and write from and into the state
- injecting the platform ID so we can check if we're in the server or the browser
- checking if we running in the browser
- checking if the data was already given to us by the server
- reading the transferred data
- removing the data from the state
- checking if we are running in the server and saving the state to be sent to the client
Running in Container
Some changes need to be done your Dockerfile if you wish to run an Angular application in a docker container in SSR mode. Here is how it should look like:
FROM node:20.10.0-alpine AS build
RUN mkdir -p /app
WORKDIR /app
COPY package*.json /app/
RUN npm install
COPY . /app/
RUN npm run build
FROM node:20-slim
COPY --from=build /app/dist/client/ dist/client/
CMD ["npm", "run", "serve:ssr:client"]
EXPOSE 4000
Known Limitations
Supporting server-side rendering proposes a new paradigm when coding in an Angular project. You need to make sure, not only that your code does not introduce any issues when run on the server's side, but also all of your dependencies. Known issues when rendering on the server's side are the inability to access objects like “window”, “document”, and other DOM-related objects since these are specific to the browser.
Some of the known dependencies already started supporting SSR-friendly solutions, while others provide some workarounds driven by the community. Such workarounds are usually conditional rendering of components or importing of modules so that they are skipped on the server's side. Nonetheless, it can cause you some trouble moving forward with your project.