DEV Community

Cover image for PWAs with Angular: Being Fast
Michael Solati
Michael Solati

Posted on • Edited on • Originally published at michaelsolati.com

PWAs with Angular: Being Fast

Earlier in the week we looked at starting to turn a basic Angular application into a Progressive Web App (you can catch up here). Now that we have an application that is reliable and will load content from cache even when there's no network, let's make our application fast!


git clone --branch v1.0 https://github.com/MichaelSolati/ng-popular-movies-pwa.git
cd ng-popular-movies-pwa
npm install
Enter fullscreen mode Exit fullscreen mode

This app depends on The MovieDB's APIs. Get an API key (check this out) and put it as the moviedb environment variable in your src/environments/environment.ts and src/environments/environment.prod.ts.

Lets run our application npm run start:pwa, and then disable JavaScript in our browser. All our user would get is a black screen:

No JavaScript

This is definitely not PWA behavior, and actually ties back to our last topic of having a reliable application. So lets fix that with one of the tools in our ng-pwa-tools package we added to our application last time. Specifically we will be using the ngu-app-shell tool.

First, we're going to go into src/app/app.module.ts file and change our BrowserModule import on line 22 to BrowserModule.withServerTransition({ appId: 'ng-popular-movies-pwa' }). (The withServerTransition() function configures our browser based application to transition from a pre-rendered page, details to come) Now lets run our ngu-app-shell.

./node_modules/.bin/ngu-app-shell --module src/app/app.module.ts
Enter fullscreen mode Exit fullscreen mode

You should have seen logged into your terminal our entire home route rendered out! We have all of our HTML, CSS, and even data grabbed from The MovieDB. What our ngu-app-shell did was prerender out our index route much in the same way that Angular Universal does.

With a prerendered home route, we don't need to worry if our user has JavaScript disabled, or if it takes a while for our JS bundles to download and execute. We have content already rendered into HTML. So we can use the ngu-app-shell to replace our empty dist/index.html with a rendered out page.

./node_modules/.bin/ngu-app-shell --module src/app/app.module.ts \
   --out dist/index.html
Enter fullscreen mode Exit fullscreen mode

While we're here, let's update our npm scripts to the following.

{
    "ng": "ng",
    "start": "ng serve",
    "start:pwa": "npm run build && cd dist && http-server",
    "build": "ng build --prod && npm run ngu-app-shell && npm run ngu-sw-manifest",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e",
    "ngu-app-shell": "./node_modules/.bin/ngu-app-shell --module src/app/app.module.ts --out dist/index.html",
    "ngu-sw-manifest": "./node_modules/.bin/ngu-sw-manifest --module src/app/app.module.ts --out dist/ngsw-manifest.json"
}
Enter fullscreen mode Exit fullscreen mode

App rendered with no JavaScript

Not only is this a better experience for when our user has JavaScript disabled, this is an inherently faster process. When we pass an already rendered page to the user, we do not need to wait for our code to run. Instead we give the user something as soon as the HTML loads, then we let our BrowserModule transition in our Angular app to replace the rendered content.


Another way we can speed up our application is "lazy loading" parts of our application. In Angular we can lazy load modules, which essentially means we can group related pieces of code together and load those pieces on demand. Lazy loading modules decreases the startup time because it doesn't need to load everything at once, only what the user needs to see when the app first loads.

In our current structure we only have two routes, one module, and essentially two components (I'm excluding the AppComponent because all it does is provide our navigation bar). So let's create a new module for our HomeComponent and our MovieComponent and put the components into those modules.

ng g m home
ng g m movie
Enter fullscreen mode Exit fullscreen mode

Next let's change our src/app/home/home.module.ts to look like this.

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MaterialModule } from '@angular/material';
import { RouterModule } from '@angular/router';

import { HomeComponent } from './home.component';

@NgModule({
  declarations: [
    HomeComponent
  ],
  imports: [
    CommonModule,
    MaterialModule,
    RouterModule.forChild([
      { path: '', pathMatch: 'full', component: HomeComponent }
    ])
  ]
})
export class MovieModule { }
Enter fullscreen mode Exit fullscreen mode

Now we'll change our src/app/movie/movie.module.ts, making it similar to our HomeModule.

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MaterialModule } from '@angular/material';
import { RouterModule } from '@angular/router';

import { MovieComponent } from './movie.component';

@NgModule({
  declarations: [
    MovieComponent
  ],
  imports: [
    CommonModule,
    MaterialModule,
    RouterModule.forChild([
      { path: '', pathMatch: 'full', component: MovieComponent }
    ])
  ]
})
export class MovieModule { }
Enter fullscreen mode Exit fullscreen mode

We should also update src/app/app-routing.module.ts to reflect that we will be lazily loading our routes from our modules.

import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  {
    path: '',
    loadChildren: 'app/home/home.module#HomeModule'
  }, {
    path: 'movie/:id',
    loadChildren: 'app/movie/movie.module#MovieModule'
  }, {
    path: 'movie',
    redirectTo: '/',
    pathMatch: 'full'
  }, {
    path: '**',
    redirectTo: '/'
  }
];

export const routing = RouterModule.forRoot(routes);
Enter fullscreen mode Exit fullscreen mode

Finally we'll update our src/app/app.module.ts to reflect our new routing, as well as remove any reference of our components.

import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgModule } from '@angular/core';
import { HttpModule } from '@angular/http';
import { MaterialModule } from '@angular/material';

import { MoviesService } from './services/movies.service';
import { NavbarService } from './services/navbar.service';

import { routing } from './app-routing.module';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule.withServerTransition({ appId: 'ng-popular-movies-pwa' }),
    HttpModule,
    BrowserAnimationsModule,
    MaterialModule,
    routing
  ],
  providers: [
    MoviesService,
    NavbarService
  ],
  bootstrap: [AppComponent]
})
export class AppModule {
  constructor(private _moviesService: MoviesService, private _navbarService: NavbarService) { }
}
Enter fullscreen mode Exit fullscreen mode

By prerendering our home route as well as lazy loading all of our routes we were able to not only make our application faster but also more reliable! While this application may not have many routes that lazy loading can shave off seconds from our initial load time, for your bigger applications it definitely will.

By running the Lighthouse tests on our current app, we can see our PWA and Performance scores nudge up from 36 each (taking the score from the previous article which was not using a deployed app), to 45 and 61 respectively.

Better Lighthouse scores


You can look at the changes we made in our code by clicking here. Additionally, if you run Lighthouse on a deployed version of our application you'll start to get results looking like this:

Deployed Lighthouse test


Final part of the series, titled "PWAs with Angular: Being Engaging," is available here.

Top comments (2)

Collapse
 
playground profile image
playground

Hi Michael,

When I try to apply app-shell as such

ngu-app-shell.js --module src/app/app.module.ts --out dist/index.html

(function (exports, require, module, __filename, __dirname) { import { Inject, Injectable, NgModule } from '@angular/core';
^
SyntaxError: Unexpected token import
at createScript (vm.js:53:10)
at Object.runInThisContext (vm.js:95:10)
at Module._compile (module.js:543:28)
at Object.Module._extensions..js (module.js:580:10)
at Module.load (module.js:488:32)
at tryModuleLoad (module.js:447:12)
at Function.Module._load (module.js:439:3)
at Module.require (module.js:498:17)
at require (internal/module.js:20:19)
at Object. (/Users/sandbox/src/app/app.module.ts:6:1)

app.module.ts:6:1 where it is referencing my private npm module

Collapse
 
michaelsolati profile image
Michael Solati

What machine are you running your code on? There have been a slew of issues and difficulties faced by windows users. Also if you have your code up on github or something like that I can take a peak and see whats going on... There can be a slew of moving parts here that can throw off your results.