To boost perceived startup performance, we want to show to the user the above the fold content as quickly as possible, and this usually means showing a menu navigation bar, the overall skeleton of the page, a loading indicator and other page-specific elements.
To do that, we will include the HTML and CSS for those elements directly in the initial HTTP response that we get back from the server when we are loading the index.html of our Single Page Application.
That combination of a limited number of above the fold plain HTML and styles which is displayed to the user as fast as possible is known as the Application Shell.
And in this post, we will learn all about how to add an App Shell to an Angular Application, using the Angular CLI!
With a couple of commands, the CLI will give us a working application with an App Shell. The first step to create an Angular application is to upgrade the Angular CLI to the latest version:
If you want to try the latest features, it's also possible to get the next upcoming version, not yet released:
And with this in place, we can now scaffold an Angular application. It's essential for the App Shell to work to have the Angular Router set up, and we will understand why in a moment.
We can include the router in the new application using the following command:
And this will create a new folder named my-app-shell with a new Angular application which includes the Router already set up.
In order to understand what problem the App Shell is solving, let's have a look at how the application works before including an App Shell.
Let's start by building this initial application in production mode:
Now we have the production application in the dist folder. If we have a look at the index.html file, here is what we have:
As we can see, this page is a blank page that contains only the following:
This means that when this page is first loaded, for a few seconds the user will not see anything. There will be an initial browser paint, but it's not a meaningful paint: the page is empty!
All the content is going to be added to the page via Javascript, everything is dynamic content and there is no static content. Let's confirm this by starting the application and seeing what is going on using the Chrome Dev Tools.
Let's start the application in production mode:
Then we will go to localhost:4200 and measure the page startup performance:
let's open the Dev Tools and select the Performance tab
let's leave the "Screenshots" checkbox checked
in the Performance tab, let's hit the "Start Profiling and Reload Page" button
We stop the recording as soon as we see something on the page
Now let's have a look at the profiling results:
As we can see, the browser is rendering the page at about 1000ms (rendering is shown in purple). There was a first paint attempt at about 600ms, but the problem is that there was no content to be displayed yet, so the page remained blank.
And this is the best case scenario of a Hello World application, as a typical SPA will render the first results later than that!
Let's then see how we can improve this.
The only way to improve things is to serve some more HTML and CSS in the body of the index.html. This is because in this very early stage of page load Angular is not yet running and in fact, the Angular bundles are still being downloaded!
To do that, we would like to take at least a good part the content of app.component.ts main application component HTML and CSS output, and move it to index.html. This should include the main skeleton of the page, including the navigation system.
But if we look into the template of the component, we will see that it has a router outlet in it:
So we need to do is to pre-render this component, and get the HTML and CSS output for the App Shell, but we need to specify what we want to put in place of the router outlet.
We are going to pre-render the main component at build time using Angular Universal, and use the pre-rendering output in our index.html.
But in place of the router outlet, we probably want to put something lighter than the full content of the / home route, because that might include too much HTML and CSS.
Instead, in place of the router outlet, we probably only want to show a loading indicator or a simplified version of the page instead of the whole home route.
The simplest way to do that is to create an auxiliary route in our application, for example in the path /app-shell-path. Then we need to pre-render the complete content of that route and include it in our index.html, and we have our App Shell!
In order to do pre-rendering in Angular, we will need Angular Universal. Let's then scaffold an Angular Universal application, that contains the same components as our client-side single page application.
We can add pre-rendering capabilities to our application, by running the following Angular CLI command:
We can find the client project name inside the angular.json CLI configuration file. Let's remember that now a CLI application can contain multiple client projects, so we need to identify the correct one.
And here is the command output:
As we can see, this command added a new build configuration entry in the Angular CLI angular.json configuration file, introducing a new application named ngu-app-shell.
This means that now we can pre-render our application using renderModuleFactory. Pre-rendering can be used in multiple ways, for example:
we can use pre-rendering in a backend Node server like Express (see instructions), to serve fully server-side rendered routes directly to the browser.
Angular will then bootstrap itself and take over the page as a normal SPA
or we can call pre-rendering from a command line tool, and build a plain HTML version of a page that we then upload and server from a CDN like Amazon Cloudfront In our case, we are going to use pre-rendering as a command line tool, by pre-rendering the HTML and CSS of our App Shell.
We can add an App Shell to our application using the following command:
Let's break down this command to see what is going on:
Lets have a look at the command output:
As we can see, we have just created a new component called app-shell! This component was then linked to the /app-shell-path route, but only in the Angular Universal application.
This /app-shell-path special route is just an internal Angular CLI mechanism for generating the App Shell, the application users will not be able to navigate to this route. In this case, this route is a build time auxiliary construct only.
Here is the routing configuration that was added only in the app.server.module.ts file (and not on the main app.module.ts):
As we can see, the /app-shell-path route is linked to AppShellComponent, which will be added in place of the router-outlet tag. The AppShellComponent is a normal scaffolded Angular component, just like any component that we obtain using ng generate.
We can edit it to include the content that we would like to display in the body of the App Shell. Here is an example that uses a loading indicator:
Besides configuring the App shell route and component, we also have some new configuration in the angular.json file:
As we can see, we have added to the build configuration of our production Angular application some configuration that says:
Pre-render the route app-shell-path using the Angular Universal application named ngu-app-shell, and use that as the App Shell
So everything is setup and ready to go, let's then build our application, see it in action and measure the performance improvements.
Let's now run the app shell build! Let's say that your project is named app-shell-test, which is the value specified on top of your angular.json file.
We can now build the App Shell by running the following command:
This time around, the content of index.html generated in the dist folder looks a lot different. Let's have a look:
As we can see, this is no longer a blank page. The styles for the AppShellComponent were added inline in the page (as usual), and the HTML for the navigation menu and the loading indicator is also present on the page.
So what happened here? The Angular CLI has taken the output of pre-rendering the App shell route, and it added that HTML output inside the index.html file.
So it looks like everything worked, and we have an App Shell ready to use!
Let's now run our application in production mode and see the results. We can run a build that is as close to production by running:
We can also do the following, let's cd into the directory and run the application using a simple HTTP server:
With the server running, let's head over to localhost:8080 and do some profiling. Let's see how soon is the app shell visible to the user:
As we can see, in this particular case the App Shell is visible at around 660ms, which represents a huge improvement to the typical time to first paint of a full SPA, which could be a couple of seconds!
Even in the case of this Hello World example we have a time to first paint that is almost half the initial time, so imagine the gains in a full-blown SPA.
This can be even further improved in several ways:
Each application needs to be optimized separately depending on how much content do we need to show to the user, and the App Shell mechanism gives us the foundation for doing that and achieving that super fast perceived startup time that we are looking for.
The built-in App Shell mechanism in the Angular CLI is a hugely beneficial performance improvement for any application (not only mobile), that is working right out of the box.
From the user perspective, a time to first paint of about half a second just feels almost instantaneous, even though in reality the application is still loading and fetching data from the backend.
The exact time to first paint will depend on each application, and the App Shell feature gives us all the tools needed to get it as low as possible.
Although this App shell mechanism is usually tied to PWAs, a PWA is not necessary to benefit from the App Shell Angular CLI features, as these two progressive improvements are configurable separately.
I hope that this post helps with getting started with the Angular App Shell and that you enjoyed it!
If you would like to learn a lot more Angular Progressive Web Applications, we recommend checking the Angular PWA Course, where we cover PWAs in much more detail.
If you have some questions or comments please let me know in the comments below and I will get back to you.