A comparison of Server Side Rendering in React and Angular applications

Kashyap Mukkamala
Level Up Coding
Published in
21 min readSep 26, 2018

--

In this article, we will talk about what Server Side Rendering(SSR) is and discuss how Server Side Rendering (SSR) can be achieved in both React and Angular applications. Towards the end, we will also do a brief comparison of the ease of enabling SSR and the approach that we need to take to do so.

To understand SSR in both these frameworks, we will create a sample application with some basic routes and make an API call to simulate a real-world scenario. We will then enable SSR for each of sample applications while discussing any workarounds or quirks necessary for achieving our intended outcome.

This article does not compare the frameworks themselves but rather the changes needed to achieve an outcome, Server Side Rendering in this case.

What is Server Side Rendering (SSR)?

With Single Page Apps (SPAs) taking over, the amount of content that is hardcoded onto the template is minimal and a web page is split up into a lot of different components and/or templates. These components are routed and loaded using custom routers which are available in all major frameworks.

Search engines (a.k.a. web crawlers ) sometimes try to access a nested route within our application, which at times is not successful as crawlers cannot access the route without downloading and executing the JavaScript bundle (which understands and enforces our routing logic). Although some of the crawlers can download and execute JavaScript, it is always nicer to be in more control of what we serve.

Another use-case is when we want to render the page as complete as possible to account for slower internet speeds (i.e. downloading the pre-rendered index.html file as opposed to downloading the CSS and JS to render it on client) or just faster above the fold loads for better Time-To-Interactive in our application.

Server Side Rendering helps us convert parts of our extremely dynamic web applications into static web applications in which we create and render the content of the requested route on the server side. This static page acts as a placeholder while the rest of application (CSS, JS etc.) are downloaded and bootstrapped in the background.

The SSR’ed page thus acts as a “splash screen” for our application while the client downloads and renders the actual application behind the scenes. Therefore, there are times when we can see the page but can not interact with it yet as the page is still loading in the background.

When to use SSR?

It is good to understand when SSR is of value and when it is not.

Implementing SSR from day 1 of the project is easy enough if we are ok with making changes to support SSR as our project progresses (as we will see in the example below), but in case the existing application is extremely complex, please evaluate the need for SSR before implementing. Here are a few quick rules that I typically use:

  1. Is the entire application hidden behind authentication? If yes, no point in doing SSR for the sake of Search Engine Optimization (SEO). However, to make the application load faster, some still choose to do SSR but my preference is to rely on service workers (depending on use case) for caching and enhancing page loads.
  2. Can the content be made static? For example, if we have a demo page that we want indexed by web crawlers, can the content be hardcoded into the templates? Can these templates be accessed directly without use of a router? Can we try to preload these resources?
  3. There are also cons to consider such as increased complexity of the application, increased initial page size, slower response from the server (since it no longer returns a lean HTML page which gets constructed on the client).

Once we discuss a few (or more) of the scenarios listed above, we can get a better understanding of how we want our users to interact with our application, how important SEO is going to be and how we can use SSR to enhance the overall experience.

SSR in Angular applications

In Angular applications, Server Side Rendering can be enabled using Angular Universal.

Before we get started with the code, let us take a quick look at what has to change within our regular application structure and what we need to be wary of when we are trying to use SSR in Angular applications.

  1. We now need a server — instead of using something like NGINX to serve our dist folder, we will now use a server (NodeJS/Express in this example) for the same to enable SSR.
  2. We need two main modules — one for client and one for server
  3. We need to make our API calls with absolute URL instead of relative URLs
  4. Caching API calls to avoid reload data loaded during SSR
  5. Some other gotchas here.

With that out of the way, we are now ready to get our hands dirty.

Basic Application Setup

First, create an Angular application using the @angular/cli npm package.

ng new ng-ssrcd ng-ssr

Next, create some base components which we will route to in our application:

ng generate component home
ng generate component about
ng generate component settings

Then, create a file in the src/app folder called app.routing.ts and setup the basic routing necessary to reach these components as shown below:

Finally, include the newly created AppRoutingModule in the main application module found in app.module.ts under the imports section after the BrowserModule. Also add links in our app.component.html to navigate to each of these components based on the route definition.

With these changes in, our basic application is ready to be started. Run the command npm start from the root of the project and navigate to http://localhost:4200 to see your application running.

Server Side Rendering Setup

The SSR setup could be broken down to 3 high-level stages:

  1. Make application SSR compatible
  2. Generate application bundle which can work in SSR and non SSR mode
  3. Server side logic to serve SSR’ed templates.

Make application SSR compatible

For the 1st stage, we need to install all the necessary dependencies to enable SSR as shown below:

npm i -S @angular/platform-server @nguniversal/express-engine @nguniversal/module-map-ngfactory-loader 

Next, let us create the server module which is a wrapper around the application module. So far, we have used AppModule only to bootstrap our application on the client:

At this stage, we are only importing the ServerModule and the ModuleMapLoaderModule and defining which component to bootstrap. Both of these imported modules will be useful when we try to access and render the application on the server.

Since we already have imported the AppModule which imports/declares/exports/provides everything else that our application needs, we only provide the Services in this module based on their need for the Server Side Rendering.

When our application bootstraps on the client, it typically goes through the following initialization step as defined in main.ts

platformBrowserDynamic().bootstrapModule(AppModule).catch(err => console.error(err));

Since we want things to be handled a little different now, we need to execute our server module when rendering on the server. To facilitate that, we create a main.server.ts alongside main.ts and then export our new AppServerModule from it. This is mostly to keep the changes for SSR as consistent as possible with the client side.

Next, we need a way to tell Angular how to compile and use these changes. We can do that by extending the existing tsconfig.json file and overriding only the entryModule to point to our new server module.

Last but not the least, we need to tell the clients main module that it has to transition from a server side rendered app to a client rendered application using the following change in the AppModule:

BrowserModule.withServerTransition({ appId: ‘ng-ssr’ }),

Instead of providing only the BrowserModule like we did till now, we now call it withServerTransition and pass a unique application id indicating what application it is (this needs to match the project name in angular.json file listed below). This removes all the CSS which were applied during the server side rendering from the page once the client bootstraps the application.

With these changes in, our code can now be used with client and server side setup. We are now ready to compile and generate the right distribution bundles which can be served from our new server.

Generate application bundle

In the next stage, we will have to tweak the current application configuration a little bit to accommodate the new files that we created in the previous stage. Currently, when we run the build command, all the packaged files are placed directly in the dist folder.

But, we now need to persist the final application bundles separately for the client and server side entry modules. We do this as our server side bundle has a different entry point and is going to be much leaner than the client since it only requires a fraction of the functionality compared to the client.

For making this change, we need to look into our angular.json file which contains all the configuration schema for the application. In angular.json, we have an entry called projects (yes, Angular 6+ supports multiple projects in same repository) which contains an entry for our ng-ssr project.

Under our ng-ssr project, we can rightfully see that there are different targets available based on the operation we can perform. One of which, build, is what we will first modify to make sure that the distribution code generated is under /dist/browser instead of its default location. To make this change, update the outputPath under the under build.options.

"architect": {    "build": {         "options": {            "outputPath": "dist/browser",

This ensures that when we run the regular ng build command, the bundle generated is under the dist/browser folder.

For running the SSR enabled code on the server we need to generate our distribution bundle with our server module as entry point. For that, we need to add a new target called server along side other targets listed in the ng-ssr project.

Under options, we have listed tsConfig which points to the file we created earlier and the new outputPath where we want the compiled code to reside. We have also listed the path to the main file which needs to be executed for the server side with the main property.

To generate the client specific dist folder we can simply run the ng build --prod command which will now be placed under the new path we defined. For compiling the code to executing on the server, we need to run the ng run command with the specific target we want it to use. In this case it is going to be ng run ng-ssr:server.

One thing to note is that since Angular 6.x everything is schematic based in Angular and Angular CLI runs these schematics, except some default schematics such as build, test, serve etc. all the schematics need to be invoked with the ng run command by passing additional parameters as shown above. And that is the role of @angular-devkit/build-angular in the builder specified above.

One important difference between the code generated for the client builds and the server builds is that a lot of the client specific code is no longer needed for example, the base index.html file, the polyfills can all be ignored since now we only render the application straight on the server and then patch it onto the views before returning.

To simplify things, we can update our package.json file with a new script to generate the dist files as necessary:

"build:ssr": "npm run build:client && npm run build:server","build:client": "ng build --prod","build:server": "ng run ng-ssr:server",

Server Side Logic

Final stage, stage 3, is the one in which we write our Express server to render and serve the files from our distribution bundle. We will code this file using TypeScript as well so to compile it we need a separate mini webpack config file which I am skipping for brevity. The first iteration of our server is similar to what can be found on the Angular documentation relating to universal rendering. Place this file at the root of the project.

The most important part of this file is setting up of the application engine from line 27–32. Here is an excerpt from the Angular documentation explaining the same:

The ngExpressEngine is a wrapper around the universal's renderModuleFactory function that turns a client's requests into server-rendered HTML pages. You'll call that function within a template engine that's appropriate for your server stack.

The first parameter is the AppServerModule. It's the bridge between the Universal server-side renderer and your application.

The second parameter is the extraProviders. It is an optional Angular dependency injection providers, applicable when running on this server.

You supply extraProviders when your app needs information that can only be determined by the currently running server instance.

The two modules which we included in our server module help deliver the AppServerModuleNgFactory — which is the machine understandable translation of our module and LAZY_MODULE_MAP — which is essentially providing all the lazy loaded modules at the initialization instead of the runtime. See more details here.

We are now ready to compile and serve the changes that we have in our application with complete server side rendering support. But before we do that, let us also add one API call to fetch data from an API server and render into onto our template.

Handling API calls with SSR

Since no application is complete without API calls, we will also implement a sample api call to fetch data from https://jsonplaceholder.typicode.com/posts and show it on the UI. This would also help us understand how to make API calls on the server side and render them to the template before returning the response back to the client.

To enable HTTP calls, we first need to import the HttpClientModule in the AppModule so that we can make the API calls. Then we include the HttpClient in the component of our choice to dispatch the actual API requests.

Http Calls in Non SSR mode

This is especially helpful during local development. Since we are making requests to a third party server, we can easily do it via proxy which can be enabled in Angular simply by adding a proxy configuration. Since the api server does not have a /api prefix to the calls, we will add it temporarily and remove it right before proxying so that we can differentiate between the API calls and the UI state changes (for consistency between SSR and non-SSR modes).

For the proxy configuration, we create a JSON file at the root of our project as shown below:

Notice the pathRewrite option above which indicates that we are stripping out the /api prefix that we will add to the requests.

And to enable proxy, we need to change our start script in package.json to the following:

"start": "ng serve --proxy-config proxy.conf.json",

We can now add our logic in any component to fetch the data, below is an example for HomeComponent:

and to show these posts on the template:

Http Calls in SSR mode

In SSR mode, the API calls are triggered in two ways:

  1. When the template is initially rendered on the server i.e. component gets loaded
  2. When the template is rendered on the client (i.e. while transitioning out of the server rendered template)

The only difference between two ways listed above is how the API calls is triggered and handled.

Since we are making the API call directly from the template on the server, we need to provide an absolute URL while making the request.

In the next stage, since our template makes the API call from the client, we can capture the request on our web server and proxy it to the API server thus bypassing CORS limitations.

For #1, API calls straight from the server, we need to update the application to able to communication with the API server directly, we can do that by passing in a new provider APP_BASE_HREF in the Server Module with the base URL value of our API server and then consume it in our API calls as shown below:

we can then inject and use it in our HomeComponent as shown below:

We are checking if we have a PLATFORM_ID only for the log statement, this can be removed if necessary.

The URL construction above is where the magic happens. On the server, when we are making a request straight out of the template, we skip the the /api prefix and when we do it from the client, we append it and let our Express server capture, modify and proxy the request for us.

Since the API call will be made on the client and server a few seconds apart, it is strongly recommended that you implement some form of caching to make the API loads faster or use a state store such as ngrx.

For #2, when the server side rendered template is being transitioned out by the client version, we need to update the way our server works. We now need to proxy the requests from our web server to an external API server. To achieve this, we can pipe the requests which Express allows. We can use any HTTP requests library (such as request in this case) to facilitate the piping:

npm i -S request

And then we replace the existing API logic as shown below in our server.ts file:

This captures all the requests which start with the prefix /api and then removes it from the URL before forwarding the request to the our API server.

Build And Run

The last thing to do before we start our server would be to compile it (since we have written it in typescript). With all the changes incorporated, the final state of the scripts section of the package.json file is as follows:

To build and serve the project now, run the following commands:

npm run build:ssrnpm run serve:ssr

Since we have added the log statements, we should be able to see the logs as shown below on the server:

And on the browser:

We can see that one second after the template is rendered and returned to the client, the client transitions out from the SSR provided template to regular application and re-renders the route.

Any and all subsequent navigations between the routes is all going to be performed in the client and would only be rendered on the server if a full page reload is performed.

Entire code base for the project can be found here and the changes specific to SSR can be found here.

SSR in React applications

In React application, for simple projects, the setup is pretty minimal. However, the tricky part is to handle the multiple libraries which we may end up using in our project. We could run into potential issues if wish to render a route on the server side which uses an incompatible library. Luckily, most common and frequently used libraries all provide SSR capabilities so we should be fine in majority of the cases. For the sake of simplicity, we will not be including a lot of libraries in our sample application below.

Similar to the Angular section, let us list out the changes that we will have to introduce so that our application is SSR compatible:

  1. We now need a server — instead of using something like NGINX to serve our build folder, we will now use a server (NodeJS/Express in this example) for the same to enable SSR.
  2. Mandatory use of Redux (or the likes) if we want to show API data which is SSR’ed
  3. Conditional checks to avoid reloading of data which was loaded on server during SSR and updated in the store
  4. Static declaration of API calls on components to facilitate SSR

Basic Application Setup

Similar to the Angular application, we first create the react application using create-react-app which consists of a few basic routes.

We will be using create-react-app to create the application. Let’s begin:

create-react-app react-ssr
cd react-ssr

Let’s add the necessary libraries to enable routing

npm i -S react-router react-router-dom

Next, let us set up the basic components which we wish to load with each of the routes, at this stage they all look alike as shown below:

Let us also create a routing file that we can read and display on our base component which is in App.js. This file could also be where we provide additional information in case we use something like react-helmet.

Update App.js to show the routes listed above:

The application can now be started and we can navigate to all the routes defined above.

Server Side Rendering Setup

The setup necessary for making the application SSR ready is fairly simple in React as compared to Angular (at least, when there are no API calls to make). There is no extra code or any special jazz needed to make the application render on the server.

What we do need is a server (Express in this case), which can serve static files and understand what route the user is trying to access, set that route as the application location and then render the application. We can also pass additional context in case we need it in the form of static context. All of this is provided by the react-router library in the form of StaticRouter as opposed to the BrowserRouter which is generally used on the client side.

Let us create folder server and add the following server.js file to it which contains 3 different middleware for loading the static resources and handling our routing logic:

since there is some JSX involved, we will install and use @babel/core, @babel/cli, @babel/preset-env and @babel/preset-react so that node can understand our server.js file:

npm i -S express @babel/core @babel/cli @babel/preset-env @babel/preset-react

Then to compile this server.js file, we can invoke a small babel script inline as shown below:

babel server/server.js --out-file server/index.js --presets=@babel/env,@babel/react

Once the server is compiled successfully, we are now ready to run our application. But before we do that, we will need to ignore the styles of our components when rendering our template on the server side, the styles will however be applied from the main.css file that is generated and placed in the build folder as a part of our final distribution that is created. To ignore these styles, we could use the ignore-styles npm plugin.

Ignoring styles would mean that when the server initially returns our template, it would be not be styled, which leads to flicker of the page once it is rendered on the client, for now, we are going to ignore this issue but it can be fixed using webpack and isomorphic style loader which can be seen from this very simple example.

Since our compiled server file is still trying to access App module we need to compile the code for which we can use the @babel/register plugin and provide the necessary presets to compile at runtime.

Put this all together and start the server. We can use the below script labelled start.js at the root of the project.

We can now add this invocation to the package.json under scripts to make things easier for us.

Handling API calls with SSR

Before we run and test our changes, let us also add a simple API call which retrieves the data from http://jsonplaceholder.typicode.com/posts as we did in case of the Angular example earlier.

To make API calls, we will use Axios npm package. Typically, to render API response on the UI. We would make the API call once the component is mounted using componentDidMount life cycle hook and then call setState to store the response of the API call onto the component state. This setState call triggers the render method again which now has access to the updated state.

Since we are compiling our component on the server and are not really mounting it to an actual DOM, we cannot use the previously discussed approach as it does not trigger the componentDidMount life cycle on the server side. However, componentWillMount gets triggered on both the server and client side. This unfortunately cannot be used as it is an anti-pattern to cause any side-effects in the componentWillMount life-cycle hook.

The simplest solution to this problem is to use a state store like Redux.

Let us first talk about how it would work on the client side:

  1. Component loads and calls componentDidMount
  2. We check if data already exists in store, if not make the API call and update store
  3. Render the component

The same flow on the server side is going to be a little different:

  1. Determine which route a user is loading
  2. Determine the component which is associated with this route
  3. If there is a static data fetch method attached to retrieve data, do it.
  4. Update the store with the data retrieved
  5. Render the template with the store data
  6. Return the template and the store that has been created on the server.

Before we make any code changes, let us add all the necessary libraries.

npm i -S axios redux redux-thunk request

Http Calls in Non SSR mode

Since we want our application to run in both SSR mode (in production) and in non SSR mode (for local development). We can simply add a proxy to our package.json file to proxy our requests (similar to the Angular application) as shown below:

We can now make our API call in the componentDidMount life-cycle hook for any component of our choice.

And this works fine in the client only mode. Since we need to account for SSR mode as well, we have to modify the component to use Redux store to hold the posts which can be used on both the server and the browser.

To facilitate that, we need to create our Redux store first which in this case is pretty lean as shown below:

In the above case the actions, reducers, and the API calls are all in the same file for readability but you might want to keep this separated in your project.

We can now call the fetchPosts method from the Home component.

The only peculiar thing in this file is the static declaration of the API method onto the class, we will discuss the need for this in the next section.

Another weird quirk is that we can no longer define state at the root element like we usually do:

state = {
something: ''
};

This gives a runtime error which asks us to install @babel/plugin-proposal-class-properties. Instead, we need to wrap it within our constructor as shown below:

constructor(props) {
super(props);
this.state = {
something: ''
};
}

The last piece of change to tie all this together is the creation of the store when the application renders on the browser:

Http Calls in SSR mode

To enable API calls on the server, we can easily build on the changes laid down for the client side. The only modification needed would be to pass a valid default state (i.e. the data which was loaded on the server) to the store when the application is being rendered on the client.

To calculate the accurate state of the store on the server side, we need to first determine the actions which need to be dispatched (which can update our store with the necessary data). Our server with the change would appear as follows:

In our example, we are hydrating our main template with a single API call. In case we had multiple, we can simply add all the requests to the serverSideFetch method and that would ensure that all the data is loaded onto the store before the template is rendered.

One more thing to notice is that at the end, when we are rendering the template, we are also adding the current state of the store to window.REDUX_DATA. That way, when the application initializes on the client, we can simply read the value on the window object and pass it into our store creation. That way, we get to avoid duplicate calls on the client for the same data which was rendered on the server.

Also, we need to modify our base index.html file in the public folder to add the placeholder for the REDUX_DATA which we will replace on the server:

Build And Run

With those changes in, we are now ready to run our application using the scripts defined in package.json.

npm run build:ssrnpm run serve:ssr

On the client, we can see that the initial request that we triggered comes back with the data loaded on the server. And on the server, we see the log statements printed as expected.

Full code base for the example shown above is available here.

Comparison & Conclusion

Although both React and Angular are very different in terms of their core principals and building blocks, one thing that is common between the two is that enabling SSR is an easy task.

When compared to React, the amount of boiler plate code to be added in an Angular app is more since adding a server specific module which encapsulates the browser main module comes in a few different files.

The server side logic for SSR is very similar in both Angular and React thanks to the fact that Angular has wrapped Express and provided us with ExpressEngine that exposes easy to use methods (at least for NodeJS based backend).

In both Angular and React apps we require a compilation step on the server side, although it can be bypassed in Angular if we are using vanilla JavaScript instead of TypeScript but for React application we end up using babel since we introduce JSX in our server side code.

Since a lot of the functionality in React comes from additional libraries (router, redux etc.), we need to explicitly integrate them on the server side whereas an Angular application only needs access to the compiled main modules factory.

Making an API call is pretty much the same complexity in Angular and React applications. But, its only true if our React application is very basic, a majority of the project do end up needing some form of state store with growing complexity so using Redux in this case is definitely not a negative.

The code base for both the examples shown above are available here: Angular, React

If you enjoyed this blog be sure to give it a few claps, read more or follow me on LinkedIn.

--

--