Hey! I'm Mark Pieszak

* Co-Founder of
   Development | Consulting | Training


   - We help elevate teams & projects
     Angular | JavaScript | Node | NestJS | ASP.NET

 

 Open Source

- ASP.NET

- Angular

* Currently located in 🌞 Florida

@Trilon_io

 

Find us Online at https://trilon.io

Why Server-side Rendering?

How do I get it today?

SSR

CSR

SEO ✔
Faster initial paint
Social Media Link Previews
Great user experience
Fully interactive

What is Isomorphic JavaScript?

JavaScript
Application
<code />

Same JavaScript Code running
on the Server

Same JavaScript Code running
on the Browser

Angular
Server-Side Rendering

(nicknamed)

Rendering Options

Dynamic Rendering

Static Pre-rendering

&

Static Pre-rendering

Done at BUILD-time

  - Great for Static sites

  - Each route will have its content pre-rendered into an .html file

 

  - Simply `serve` up the
dist/PROJECT_NAME folder.


/ dist /

    / my-project / 
    
        index.html
        about.html
        contactus.html

Static Pre-Rendering

Request

Response

Dynamic Rendering

Done at RUN-time

  - This is more typical, dynamic sites where data is Http based and data is constantly changing.
 

  - Each route will render at run-time when requested and is served to the client.   

  - You must deploy the Node server


/ dist /

    / my-project /
        index.html
 
    / my-project-server / 

    server.js

Dynamic Rendering

Request

Response

Getting Started

What options do we have?

NodeJS

NestJS

ASP.NET Core

More about NestJS Today @ 2:40pm
Oceana Room 11

Getting Started w/ the CLI

$ ng new angular-mix-demo

$ ng add @nguniversal/express-engine 
  --clientProject angular-mix-demo
$ npm run build:ssr && npm run serve:ssr

CLI Project Name

Run your Universal App:

Universal App Structure

Angular App

BrowserModule

imports AppModule

imports ServerModule

app.module.ts

app.server.module.ts

How does Universal work?

Essentially lots of Dependency Injection

Possible because of Angular's

Compiler & Renderer not being tied to the Browser.




@NgModule({
  exports: [BrowserModule],
  imports: [HttpModule, HttpClientModule, NoopAnimationsModule],
  providers: [
    SERVER_RENDER_PROVIDERS,
    SERVER_HTTP_PROVIDERS,
    {provide: Testability, useValue: null},
    {provide: ViewportScroller, useClass: NullViewportScroller},
  ],
})
export class ServerModule { }

ServerModule
imports all the Dependency Injection -magic-

Universal behind the scenes

How does Universal work?

Dependency Injection Mania

Provide Use
RenderFactory2 ServerRenderFactory2
BrowserXHR ServerXHR (Zone wrapped)
DOCUMENT Domino
PlatformLocation ServerPlatformLocation
StylesHost ServerStylesHost

Use these instead

export const SERVER_RENDER_PROVIDERS: Provider[] = [
  ServerRendererFactory2,
  {
    provide: RendererFactory2,
    useFactory: instantiateServerRendererFactory,
    deps: [ServerRendererFactory2, ɵAnimationEngine, NgZone]
  },
  ServerStylesHost,
  {provide: SharedStylesHost, useExisting: ServerStylesHost},
  {provide: EVENT_MANAGER_PLUGINS, multi: true, useClass: ServerEventManagerPlugin},
];

Server -Render- Providers

Universal behind the scenes

Universal behind the scenes

export const SERVER_HTTP_PROVIDERS: Provider[] = [
  {
    provide: Http, 
    useFactory: httpFactory, 
    deps: [XHRBackend, RequestOptions]
  },

  { 
    provide: BrowserXhr, 
    useClass: ServerXhr
  }, 

  { 
    provide: XSRFStrategy, 
    useClass: ServerXsrfStrategy
  },

  { provide: XhrFactory, useClass: ServerXhr }, 
  {
    provide: HttpHandler,
    useFactory: zoneWrappedInterceptingHandler,
    deps: [HttpBackend, Injector]
  }
];

Server -Http- Providers

What does Universal look like in Node?

@nguniversal express-engine


import { ngExpressEngine } from '@nguniversal/express-engine';
import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader';

// Our Universal express-engine (github.com/angular/universal)
app.engine('html', ngExpressEngine({
    bootstrap: AppServerModuleNgFactory,
    providers: [
        provideModuleMap(LAZY_MODULE_MAP)
    ]
}));

app.set('view engine', 'html');

app.get('*', 
 (req: Request, res: Response) => {
  res.render('index', {
    req,
    res
  });
});

Notice Request/Response are passed in to the Engine which handles creating the InjectionTokens and passes them into Angular.
Now you can use them within your Angular App!

For every request (URL)

import { Injector } from '@angular/core';

import { Request, Response } from 'express';
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens'; // <--

export SomeComponent {

    private request: Request;
    private response: Response;

    constructor(
        private injector: Injector
    ) { 
        this.request = this.injector.get(REQUEST);
        this.response = this.injector.get(RESPONSE);

        // cookies / headers / anything from our original Node Request
        // same with Response
    }
}

Request/Response inside your Angular App

@nguniversal express-engine

Grab the Request/Response using the Injector

renderModuleFactory()

Angular Code bundles
(vendor / main)

Angular bootstraps via platformBrowser()

( In the background ...)

Node Server

Render App to a String
"<html>
  <app-root></app-root>
</html>"

App Code

Angular finishes bootstrapping

Now we have our
- fully functioning -
CSR Angular Application

Easy, right!?

Adding Universal to an
existing Application

What about...

Adding Universal to an existing Application

ReferenceError: window is not defined
    at new AppComponent (/Users/markpieszak/Documents/universal-talk/angularmix/dist/angularmix-server/main.js:161:9)
    at createClass (/Users/markpieszak/Documents/universal-talk/angularmix/node_modules/@angular/core/bundles/core.umd.js:9323:24)
    at createDirectiveInstance (/Users/markpieszak/Documents/universal-talk/angularmix/node_modules/@angular/core/bundles/core.umd.js:9210:24)
....

ng add schematic, wire everything up correctly.

Go to run the Node server locally, and...

ReferenceError: jQuery is not defined
    at new AppComponent (/Users/markpieszak/Documents/universal-talk/angularmix/dist/angularmix-server/main.js:200:9)
    at createClass (/Users/markpieszak/Documents/universal-talk/angularmix/node_modules/@angular/core/bundles/core.umd.js:9323:24)
    at createDirectiveInstance (/Users/markpieszak/Documents/universal-talk/angularmix/node_modules/@angular/core/bundles/core.umd.js:9210:24)
....

Adding Universal to an existing Application

#1 Universal "Gotcha"

Be aware of anything touching Browser platform specific APIs, and globals such as"window | document | ..."

Be aware of Globals

If you must use something like Window,
Create a WindowService and use Depenency Injection to provide different classes per platform.

@Injectable()
export class WindowService {
  // Some typical window use-cases
  public navigator: any = {};
  public location: any = {};
  public alert(msg: string) { return; }
  public confirm(msg: string) { return; }
  public setTimeout(handler: (...args: any[]) => void, timeout?: number): number { return 0; }
  public clearTimeout(timeoutId: number): void { }
  public setInterval(handler: (...args: any[]) => void, ms?: number, ...args: any[]): number { return 0; }
  public clearInterval(intervalId: number): void { }
  public ga(command: string | Function, params?: any, extra?: any): void { }
  // ...
}
export function win () {
  return typeof window !== 'undefined' ? window : {};
}

// NgModule providers[]
{ 
   provide: WindowService, 
   useFactory: (win) // ^ above
} 
// app.server.module.ts NgModule providers[]
{ provide: WindowService, useClass: WindowService }, 

Browser (app.module.ts)

Server (app.server.module.ts)

Other Universal "Gotchas"

Make sure you wrap all Browser-platform specific Code

import { OnInit, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser, isPlatformServer } from '@angular/common';

export class SomeComponent implements OnInit {

  private isBrowser: boolean = isPlatformBrowser(this.platformId);

  constructor(
    private window: WindowService,
    @Inject(PLATFORM_ID) private platformId: Object
  ) { }

  ngOnInit() {

    if (this.isBrowser) { 
      (this.windowService).$('body')... // something crazy involving the window
    }

  }
}

Protip: Wrap
browser-only logic to prevent errors

Node global patching & mocking

$ npm i -S @devhelponline/create-node-mocks

In Node, the top-level object is global,
we can use this to our advantage when doing SSR to patch it with things we need in order to prevent errors, or give us functionality we need!

import { createNodeMocks } from '@devhelponline/create-node-mocks';
import { readFileSync } from 'fs';
import { join } from 'path';

const template = readFileSync(join(DIST_FOLDER, 'ANGULAR_CLI_PROJECT_NAME', 'index.html'));

createNodeMocks(template /*, { additionalWindowMocks }, { additionalGlobalMocks } */);

server.ts

Using Window/Document

Server-side Render

Client-side Render

A

B

C

A

B

C

Component "B" uses
"window" or "document"

"window/document" exists in the Browser-world

Because of Dependency Injection or Mocking

Node keeps running

Common SSR Gotchas
& Tips

Platform Detection

Platform-wrapping will become so common in your app, it helps to make a nice wrapper Service around them (and other platform-specific work you'll be doing)

@Injectable()
export class PlatformService {

  get isBrowser(): boolean {
    return isPlatformBrowser(this.platformId)
  }

  get isServer(): boolean {
    return isPlatformServer(this.platformId)
  }

  constructor(
    @Inject(PLATFORM_ID) private platformId: Object
  ) { }

}

  

Common SSR Gotchas

Server-side Render

Client-side Render

A

B

C

A

B

C

Renders entire App

Renders entire App
Again

Rendering

Server-side Render

Client-side Render

A

B

C

A

B

C

Http call made here

Http call will be triggered -again-

Template "flickers" when Data goes blank then re-appears

Common SSR Gotchas

App -flickers-

How can we handle these common SSR issues in our Angular Universal Apps?

TransferHttpCacheModule

- With Angular 5.0 came the new TransferState

  - Use this so Http calls don't get hit TWICE
    Once during SSR, and again during CSR.

- Use TransferHttpCacheModule to have all GET HttpClient requests automatically transferred and re-used!

import { BrowserTransferStateModule } from '@angular/platform-browser';
import { TransferHttpCacheModule } from '@nguniversal/common';

imports: [
  // ...
  HttpClientModule,
  TransferHttpCacheModule, // <-- automatically caches HttpClient GET requests
  BrowserTransferStateModule // <--
]

import { ServerTransferStateModule } from '@angular/platform-server';
imports: [ 
  // ... 
  ServerTransferStateModule // <--
]

Browser (app.module.ts)

Server (app.server.module.ts)

Now with HttpTransferCache

Server-side Render

Client-side Render

A

B

C

A

B

C

Http call made here

Http call re-used

Data sent down with html and re-used
when CSR calls the same Http call

Other Universal "Gotchas"

If you -must- use document,
grab it from @angular/common


import { Inject } from '@angular/core';
import { DOCUMENT } from '@angular/common';

constructor(@Inject(DOCUMENT) private document) { 
  this.document.querySelector('#test');
  // do something with the DOM
}

Other Universal "Gotchas"

Be careful with setTimeout & setInterval

 

import { PLATFORM_ID } from '@angular/core';
import { PlatformService } from '../shared/platform.service';

export class SomeComponent {

  constructor(
    private window: WindowService,
    private platformService: PlatformService
  ) { }

  ngOnInit() {
    if (platformService.isBrowser) { 
      (this.windowService).setTimeout(() => { }, 1000);
    }
  }
}

They WILL delay your Server-render, as Zones will be waiting

Real-world Universal example

ChelseaFC.com

ChelseaFC.com

Cloudinary

AWS Node

Cache everything

Most users are on 3G mobile device

ChelseaFC.com

Cache everything

Problem:
With even Node (Angular Universal) responses cached, how can we still responsively deliver cloudinary images?

ChelseaFC.com

Solution:
During the SSR - Pass down all variations of image responsive sizes within an object inside a data-attribute on each element

  1. Pass down vanillaJS with index.html that quickly computes the size of the device/window and swaps in the correct image from the data-attribute for each image.
     
  2. Have Angular re-use these values during Client-rendering bootstrap, as it's aware that this is the "first Render"

SSR / Initial "paint"

CSR Finished
Full SPA Angular App

Time

Images loaded almost _instantly_

Very subtle differences

SSR Version - Cons / Trade-offs

Menu isn't fully functional yet

Correct font hasn't been swapped-in yet,
we default to a web standard version.

Image carousel is the first Static image at this point

Feedback (3rd party external library) hasn't loaded yet. (We lazy load other ones as well)

We can see our app is still loading & parsing

SSR Version - Pros

Scroll functionality still available.
Site/App is usable during this time!

RouterLink translated into
<a href>, navigation still works!

Although our main "carousel" isn't working yet, our users still get a full experience and a nice background image here.

In Conclusion

Things to Consider

  • Pass down as "little" as possible.
    • Ignore non-essential Http calls & portions of your App
       
  • Some examples:
    • Company twitter feed section
    • "Trending" products top-bar
    • Sections with (non-essential)
      real-time data
  • Use CSS as opposed to JS animations/hovers/functionality wherever possible.​
    • ie: Menu / Navigation / Animations
  • Ensure routerLink is utilized whenever possible on crucial links
    • (click) events won't function during the SSR phase
    • routerLink will become <a href> tag that can redirect during the SSR phase
  • Use font-display: swap
    • Uses back-up font while waiting for desired one to load
    • Boosts time to First Meaningful Paint
  • Be very mindful of browser-specific APIs or logic, and wrap it around isBrowser
    • setTimeout & setInterval
    • window | document | etc
  • Use createNodeMocks(template) to prevent typical window/global errors in Node (even in 3rd party libs)

Want more?

For more Gotchas, tips & tricks, and utility libraries
Visit the Universal repo:

http://www.github.com/Angular/Universal

Great Article on the nuances of SSR

https://tinyurl.com/david-east

Thank You!

Github
@MarkPieszak

 

Twitter

@MarkPieszak

Medium

@MarkPieszak
 

Trilon Consulting

Trilon.io
Consulting | Training | Workshops | Development

Angular Universal - Dark side of server-side rendering

By Mark Pieszak

Angular Universal - Dark side of server-side rendering

Deep-dive presentation on Angular Universal and common server-side rendering use-cases

  • 2,576
Loading comments...

More from Mark Pieszak