Ionic Server Side Rendering (SSR)
Guide on the importance of server side rendering and how to add it to your Ionic Angular app
Last updated
Was this helpful?
Guide on the importance of server side rendering and how to add it to your Ionic Angular app
Last updated
Was this helpful?
Universal Javascript is JavaScript that can run on the server and in the browser. This is what people refer as “Server Side Rendering” (SSR). By utilizing SSR and Universal JavaScript in our app, we can do an initial render of our app on the server and send over a precompiled version before any JavaScript has been run on the client.
So if SSR can help us, how can we add it to our app? Well up until recently, you couldn’t. A lot of the Ionic components have references to the window
object and other DOM specific APIs. Since we’re on a server and running in NodeJs, we don’t have access to the DOM. This is where Angular Universal and @ionic/angular-server
module comes in.
This feature is only available in PRO version.
is the library used for running our apps on the server.
A normal Angular application executes in the browser, rendering pages in the DOM in response to user actions. Angular Universal executes on the server, generating static application pages that later get bootstrapped on the client. This means that the application generally renders more quickly, giving users a chance to view the application layout before it becomes fully interactive.
There are three main reasons to add SSR to your app.
Better SEO rankings (facilitate web crawlers)
Improve performance (specially on mobile and low-powered devices)
Preview cards on social media
Google, Bing, Facebook, Twitter, and all social media sites rely on web crawlers to index your application content and make that content searchable on the web. Angular Universal generates a static version of your app that is easily searchable, linkable, and navigable without JavaScript. Universal also makes a site preview available since each URL returns a fully rendered page.
We created a SeoService
to allow you to change your SEO meta tags dynamically on each route. You can find this service in src/app/utils/seo/seo.service.ts.
If you want to add dynamic meta tags to a specific route you need to add some code on the route resolver.
For the Travel Listing page we added the following meta tags (you can also add an image):
For the Travel Details page we set the meta tags dynamically depending on the Data Item. For example, if you visit "Tristan Narvaja" details page, the title of that page would be: "Tristan Narvaja" and the description would be a short description of that place.
If you don't define specific meta tags for your routes, the SeoService
will use the default meta tags. Make sure to update this with your own title, description and image.
You can also use this service to set the canonical urls.
Make sure to add your own url to the property canonicalUrl
from the SeoService.
The service will then add the rest of the url path.
Server Side Rendering can really help improve the overall performance of your app. Both the intrinsic performance (optimizing the medium and resources, ie: bandwidth, requests, images, etc) and the perceived performance (how fast the user thinks your website is).
Among others, SSR is great for:
Less runtime overhead (intrinsic performance)
Because we serve static html (pre-rendered) and we have instant access to the initial state data for the app
Because the app was pre-rendered on the server, the user sees the first page quickly
We are happy to announce that after a bumpy ride with previous versions of the framework, latest releases of Ionic Angular successfully support Server Side Rendering through Angular Universal (@ionic/angular-server).
This is a substantial milestone for the framework as SSR was a much awaited feature. It fills the gap of many use cases, performance and user experience.
Upgrading your existing app to support Angular Universal
In our experience, the process is relatively simple once you figure out all the possible bugs that may arise and declutter all the misinformation about what needs to be done to have a properly configured Angular Universal app.
First of all you need to add some dependencies to your project.
We need to adjust a bit our code so it plays nice with Angular Universal.
You may encounter with a ReferenceError: XMLHttpRequest is not defined
error.
Somehow Angular Universal has issues if the HttpClient
is included in several modules. The solution is to remove HttpClientModule
from all lazy and eager loaded feature modules and move its import to the app.module.ts
file.
By default, when doing request with relative paths from the server, it will throw errors as it's unable to locate those relative paths.
By adding a custom Interceptor, we ensure requests to local assets (i.e.: ./assets/sample-data/fashion/listing.json
) will be converted to absolute paths and work OK from the server.
When it comes to Server Side Rendering, there are some best practices to take into consideration.
As mentioned before, when we enable server side rendering, we end up bundling two versions of the app.
One that's meant to run on the server (render views through NodeJs and output the Angular bootstrapped app html to the browser) and the other one that will run and get bootstrapped on the browser.
To avoid duplicate work (for example requests to fetch data from an API) it's recommended to use Angular TransferState
API to seamlessly transition between the server side rendered to the browser rendered app.
If you don't implement a TransferState
strategy you will end up performing an http request in the server, and again, the same request in the browser after transitioning from the server side rendered app.
In our case, we created a TransferStateHelper
service to help us store data from requests made using our custom DataStore
utility.
We also use this helper service to prevent showing shell elements in SSR to avoid SEO penalties and improve user experience.
You can see how we use the TransferStateHelper
in all the services for the different category pages.
We recently added a new configuration to our App Shell DataStore
utility to enable loading data without prepending a shell.
You can see how we rely on this feature to avoid showing App Shell placeholders when rendering the app in the server in all the services for the different category pages.
Assets that are requested once the app html get's rendered in the browser (images, videos) behave differently than text elements that can be requested (through an http request for example) and rendered directly on the server side (and transferred through the network as part of the app's html files).
In particular, in our case we use the TransferStateHelper
service to mark all the images included in the requested view (rendered in the server) and avoid showing loading animations on those images.
You can see how we put this in action in the <app-image-shell>
component.
When loading the app from the server, if you throttle your network speed so that the client-side scripts take longer to download (instructions below), you'll notice that you can click on routerLink
elements and navigation works correctly.
When rendering the app in the server, we lack the possibility to inspect the user's browser user-agent which has valuable information about the user's device, OS, etc. Fortunately, we can still access the information about the user-agent that's requesting the app in the Request user-agent header.
In previous projects we have used this technique for many purposes and believe it could be handy to include an example on how to access Request and Response data from NodeJs in your Ionic/Angular app.
You can see how we send data from the NodeJs server to the Angular client using custom Response headers both in the server.ts
file and the app.module.ts
(through the APP_INITIALIZER
utility).
Every time the user navigates to a route that does not exist, the Angular app will respond with the page component defined in that wildcard route, but the server (NodeJS Express) will always return a 200 status code instead of the proper 404 (not found).
In order to handle non existent pages correctly, we rely on the static-paths.ts
file to configure the server logic that handles missing routes in NodeJs on the server.ts
file.
Similarly, we should not rely purely on client side Angular Router redirects. Those are not pure redirects, they are emulated redirects because they always return a 200 status code instead of the proper 302 (temporarily redirect) or 301 (permanent redirect).
In order to handle redirects correctly, we rely on the static-paths.ts
file to configure the server redirection logic in NodeJs on the server.ts
file.
Last but not least, we should handle missing assets correctly. This means that if the app requests any missing assets, it should return a 404 status code.
If we don't enable this property, by default, the NodeJs Express server will fall through the missing assets to the next middleware, triggering the Angular app wildcard route and a 200 status code instead of the correct 404 status code.
To solve this issue, just enable this simple property in the server.ts
file.
To test the Angular Universal (SSR) implementation, start a development server with live reload included by running:
To test the production build, run:
Testing all the aspects of a Server Side Rendered app can be tricky.
In order to specifically test what the SEO crawlers see when requesting your server side rendered app, you need to disable javascript in the Developer Tools panel. This will enable you to experience the raw experience of your SSR app, without any interactivity, just plain CSS and HTML.
The transition from the server-rendered app to the client app happens quickly on a development machine, but you should always test your apps in real-world scenarios.
You can simulate a slower network to see the transition more clearly as follows:
Open the Chrome Dev Tools and go to the Network tab.
Try one of the "3G" speeds.
This way you will be able to observe how the server-rendered app still launches quickly but the full client app may take some seconds to load (due to the network throttling) while giving you the possibility to see how the transition goes.
As a final note, I will be honest and share my opinion and experience after adding SSR to a complete, medium sized app like this template.
These are some of the issues I found on the process. Some are trivial, some have minor importance and would be "nice to have", and a couple are deal breakers for a production app.
Minor visual issues (that can be fixed with workarounds)
Visual issues without workarounds
User experience issues without workarounds
Deal breakers
Maybe it's because I always thought of SSR as the windup feature of any production app, the one that over delivers on the end user experience and helps your projects in non technical related stuff like SEO or perceived performance (which may impact directly on your conversion rates and thus critical for the business).
All the stuff you really start appreciating after the project is deployed and you start seeing your app more through the business lens and less through the technical lens.
Maybe it's because after working several months on a project, built upon cumulative gains, you expect SSR to be a step forward that makes your work shine, and not a drawback.
Maybe it's because I realize the amazing work the Ionic Team has done again and again and thus always demand the best, what they are really capable of.
I have empathy and admiration for the Ionic Team, they are working really hard on so many features that sometimes it's easy to ignore or assume some other feature is not important for the bottom line.
For a demo app, at first glance everything seems to be working fine in regards to SSR, but once you start working on a more complex project and testing real world scenarios, you start finding issues that end up reducing the overall user experience and quality of your app compared to its previous state without SSR.
This may sound harsh, but it's an honest critique from one of the biggest believer and advocate of the Ionic Framework (myself, Agustin). I hope my words get voided by upcoming releases of the framework addressing the issues listed above.
Disclaimer: I tried to be helpful in the process and not just complain or submit bugs, but dig deep in the Ionic source code, analyze the root cause of the issues and by doing so help the Ionic Team reach a diagnosis of the issues easier and faster. Hope you see this post as a constructive critique.
Faster (perceived performance)
According to the official we need to enable the initialNavigation
property of the Angular Router (check the app-routing.module.ts
) for server-side rendering to work.
If you have simple http GET requests, then you can use the . This module installs an Http Interceptor that avoids doing duplicate requests that were already made when the application was rendered on the server.
Alternatively, for more advanced use cases, you can use the API directly (basically a key-value store).
As we , when talking about Progressive Web apps, we follow the App Shell pattern to show some placeholders while loading data. This technique has many benefits including improved user experience while avoiding layout shifts (check out documentation to better understand the implications).
However, user events other than routerLink
clicks aren't supported. You must wait for the full client app to bootstrap and run, or buffer the events using libraries like , which allow you to replay these events once the client-side scripts load.
We should not rely purely on Angular Router to handle non existent navigation routes.
Find the dropdown on the far right of the menu bar.
I feel this is the case for server side rendering. They have been on adding SSR support to the framework and once they reached a semi-stable state, they the big news.