Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Maciej Treder "Server-side rendering with Angular—be faster and more SEO, CDN and user-friendly!"

104 views

Published on

Are you ready for production? Are you sure? Is your application prefetchable? Is it readable for search engine robots? Will it fit into Content Delivery Network? Do you want to make it even faster? Meet the Server-Side Rendering concept. Learn how to implement it in your application and gain knowledge about best practices, such as transfer state and route resolving strategies.

Published in: Technology
  • Be the first to comment

  • Be the first to like this

Maciej Treder "Server-side rendering with Angular—be faster and more SEO, CDN and user-friendly!"

  1. 1. Angular Universal Be SEO, CDN & user friendly! Maciej Treder AKAMAI TECHNOLOGIES
  2. 2. @maciejtreder
  3. 3. @maciejtreder • Kraków, Poland • Senior Software Development Engineer in Test 
 Akamai Technologies • Angular passionate • Open source contributor (founder of @ng-toolkit) • Articles writer
  4. 4. Outline • Why SPAs are not SEO? • Server-side rendering concept • Working with server-side and browser-side code • API calls performance • What’s more? & Deployment • Prerendering & Summary
  5. 5. ng build • Ahead of Time compilation
  6. 6. We are ready for —prod! • Ahead of Time compilation • Minified • Tree-shaked
  7. 7. ng build vs. —prod
  8. 8. Common SPA problem <IfModule mod_rewrite.c> RewriteEngine On RewriteBase / RewriteRule ^index.html$ - [L] RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . index.html [L] </IfModule> .htaccess
  9. 9. Common SPA problem GET / GET /anotherPage index.html G ET /subpage G ET /contact GET /home
  10. 10. Common SPA problem GET / GET /anotherPage
  11. 11. Common SPA problem
  12. 12. Outline • Why SPAs are not SEO? • Server-side rendering concept • Working with server-side and browser-side code • API calls performance • What’s more? & Deployment • Prerendering & Summary
  13. 13. Server Side Rendering GET / GET /anotherPage
  14. 14. Is it worth? curl localhost:8080 <!DOCTYPE html><html lang="en"><head> <meta charset="utf-8"> <title>SomeProject</title> <base href="/"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" type="image/x-icon" href="favicon.ico"> <link rel="stylesheet" href="styles.3ff695c00d717f2d2a11.css"><style ng-transition="app-root"> /*# sourceMappingURL=data:application/ json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IiIsImZpbGUiOiJzcmMvYXBwL2FwcC5jb21wb25lbnQuY3NzIn0= */</ style></head> <body> <script type="text/javascript" src="runtime.26209474bfa8dc87a77c.js"></script><script type="text/javascript" src="es2015- polyfills.c5dd28b362270c767b34.js" nomodule=""></script><script type="text/javascript" src="polyfills.8bbb231b43165d65d357.js"></ script><script type="text/javascript" src="main.8a9128130a3a38dd7ee5.js"></script> <script id="app-root-state" type="application/json">{}</script></body></html> <app-root _nghost-sc0="" ng-version="7.2.9"><div _ngcontent-sc0="" style="text-align:center"><h1 _ngcontent-sc0=""> Welcome to someProject! </h1><img _ngcontent-sc0="" alt="Angular Logo" src="data:image/ svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERD AwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJ GIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1 Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogI Dwvc3ZnPg==" width="300"></div><h2 _ngcontent-sc0="">Here are some links to help you start: </h2><ul _ngcontent-sc0=""><li _ngcontent-sc0=""><h2 _ngcontent-sc0=""><a _ngcontent-sc0="" href="https://angular.io/tutorial" rel="noopener" target="_blank">Tour of Heroes</a></h2></li><li _ngcontent-sc0=""><h2 _ngcontent-sc0=""><a _ngcontent-sc0="" href="https:// angular.io/cli" rel="noopener" target="_blank">CLI Documentation</a></h2></li><li _ngcontent-sc0=""><h2 _ngcontent-sc0=""><a _ngcontent-sc0="" href="https://blog.angular.io/" rel="noopener" target="_blank">Angular blog</a></h2></li></ul></app-root>
  15. 15. Is it worth?
  16. 16. —prod vs. universal
  17. 17. Angular Universal Load HTML Bootstrap Load HTML Bootstrap SSR NO SSR First meaningful paint First meaningful paint
  18. 18. How to start? Official guide https://angular.io/guide/universal ng-toolkit https://github.com/maciejtreder/ng-toolkit
  19. 19. ng add @nguniversal/express-engine —clientProject myProject CREATE src/main.server.ts (220 bytes) CREATE src/app/app.server.module.ts (318 bytes) CREATE src/tsconfig.server.json (219 bytes) CREATE webpack.server.config.js (1360 bytes) CREATE server.ts (1500 bytes) UPDATE package.json (1876 bytes) UPDATE angular.json (4411 bytes) UPDATE src/main.ts (432 bytes) UPDATE src/app/app.module.ts (359 bytes)
  20. 20. Adjust your modules app.module.tsapp.server.module.ts @NgModule({ bootstrap: [AppComponent], imports: [ BrowserModule.withServerTransition({appId: 'my-app'}), //other imports ], }) export class AppModule {} import {NgModule} from '@angular/core'; import {ServerModule} from '@angular/platform-server'; import {ModuleMapLoaderModule} from ‘@nguniversal/module-map-ngfactory-loader'; import {AppModule} from './app.module'; import {AppComponent} from './app.component'; @NgModule({ imports: [ AppModule, ServerModule, ModuleMapLoaderModule ], bootstrap: [AppComponent], }) export class AppServerModule {}
  21. 21. Adjust your modules Official guide app.module.tsapp.server.module.ts @NgModule({ declarations: [AppComponent], imports: [ //common imports ] }) export class AppModule {} import {NgModule} from '@angular/core'; import {ServerModule} from '@angular/platform-server'; import {ModuleMapLoaderModule} from ‘@nguniversal/module-map-ngfactory-loader'; import {AppModule} from './app.module'; import {AppComponent} from './app.component'; @NgModule({ imports: [ AppModule, ServerModule, ModuleMapLoaderModule, //server specific imports ], bootstrap: [AppComponent], }) export class AppServerModule {} app.browser.module.ts @NgModule({ bootstrap: [AppComponent], imports: [ AppModule, BrowserModule.withServerTransition({appId: 'my-app'}), //browser specific imports ] }) export class AppModule {} //browser specific imports //server specific imports
  22. 22. ng add @ng-toolkit/universal CREATE local.js (248 bytes) CREATE server.ts (1546 bytes) CREATE webpack.server.config.js (1214 bytes) CREATE src/main.server.ts (249 bytes) CREATE src/tsconfig.server.json (485 bytes) CREATE src/app/app.browser.module.ts (395 bytes) CREATE src/app/app.server.module.ts (788 bytes) CREATE ng-toolkit.json (95 bytes) UPDATE package.json (1840 bytes) UPDATE angular.json (4022 bytes) UPDATE src/app/app.module.ts (417 bytes) UPDATE src/main.ts (447 bytes)
  23. 23. And let’s go! • npm run build:prod • npm run server Date: 2018-11-21T13:04:33.302Z Hash: 1a82cb687d2e22b5d12b Time: 10752ms chunk {0} runtime.ec2944dd8b20ec099bf3.js (runtime) 1.41 kB [entry] [rendered] chunk {1} main.09093ffa4ad7f66bc6ff.js (main) 169 kB [initial] [rendered] chunk {2} polyfills.c6871e56cb80756a5498.js (polyfills) 37.5 kB [initial] [rendered] chunk {3} styles.3bb2a9d4949b7dc120a9.css (styles) 0 bytes [initial] [rendered] > my-app@0.0.0 server /Users/mtreder/myApp > node local.js Listening on: http://localhost:8080Listening on: http://localhost:8080
  24. 24. Under the hood export const app = express(); app.use(compression()); app.use(cors()); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = require('./dist/server/main'); app.engine('html', ngExpressEngine({ bootstrap: AppServerModuleNgFactory, providers: [ provideModuleMap(LAZY_MODULE_MAP) ] })); server.ts
  25. 25. Under the hood app.get('/*', (req, res) => { res.render('index', {req, res}, (err, html) => { if (html) { res.send(html); } else { console.error(err); res.send(err); } }); }); server.ts
  26. 26. Under the hood app.set('view engine', 'html'); app.set('views', './dist/browser'); app.get('*.*', express.static('./dist/browser', { maxAge: '1y' })); server.ts
  27. 27. Server Side Rendering GET / GET /anotherPage
  28. 28. Outline • Why SPAs are not SEO? • Server-side rendering concept • Working with server-side and browser-side code • API calls performance • What’s more? & Deployment • Prerendering & Summary
  29. 29. Browser vs Server• document • window • navigator • file system • request
  30. 30. Browser vs Server public ngOnInit(): void { console.log(window.navigator.language); } Listening on: http://localhost:8080 ERROR ReferenceError: window is not defined at AppComponent.module.exports../src/app/app.component.ts.AppComponent.ngOnInit (/Users/mtreder/myApp/dist/server.js:118857:21) at checkAndUpdateDirectiveInline (/Users/mtreder/myApp/dist/server.js:19504:19) at checkAndUpdateNodeInline (/Users/mtreder/myApp/dist/server.js:20768:20) at checkAndUpdateNode (/Users/mtreder/myApp/dist/server.js:20730:16) at prodCheckAndUpdateNode (/Users/mtreder/myApp/dist/server.js:21271:5) at Object.updateDirectives (/Users/mtreder/myApp/dist/server.js:118833:264) at Object.updateDirectives (/Users/mtreder/myApp/dist/server.js:21059:72) at Object.checkAndUpdateView (/Users/mtreder/myApp/dist/server.js:20712:14) at ViewRef_.module.exports.ViewRef_.detectChanges (/Users/mtreder/myApp/dist/ server.js:19093:22) at /Users/mtreder/myApp/dist/server.js:15755:63 ERROR ReferenceError: window is not defined
  31. 31. is server? is browser? import { Component, Inject, PLATFORM_ID, OnInit } from '@angular/core'; import { isPlatformBrowser, isPlatformServer } from '@angular/common'; @Component({ selector: 'home-view', templateUrl: './home.component.html' }) export class HomeComponent implements OnInit { constructor( private platformId) {} public ngOnInit(): void { if ( ) { console.log('I am executed in the browser!’); // window.url can be reached here } if (isPlatformServer(this.platformId)) { console.log('I am executed in the server!’); // window.url CAN’T be reached here } } } @Inject(PLATFORM_ID) isPlatformBrowser(this.platformId) isPlatformServer(this.platformId)
  32. 32. Wrapper services • Determine if we are in the browser or server • Retrieve window or request object • Create ‘mock’ window from request object if necessary
  33. 33. REQUEST import { Component, OnInit, Inject, PLATFORM_ID, Optional } from ‘@angular/core’; import { REQUEST } from '@nguniversal/express-engine/tokens'; import { isPlatformServer } from '@angular/common'; @Component({ selector: 'app-root', templateUrl: './app.component.html', }) export class AppComponent implements OnInit { constructor( @Inject(REQUEST) private request: any, @Inject(PLATFORM_ID) private platformId: any) {} public ngOnInit(): void { if (isPlatformServer(this.platformId)) { console.log(this.request.headers); } } } import { REQUEST } from '@nguniversal/express-engine/tokens'; @Optional @Inject(REQUEST) private request: any, console.log(this.request.headers);
  34. 34. REQUEST Listening on: http://localhost:8080 { host: 'localhost:8080', connection: 'keep-alive', 'cache-control': 'max-age=0', 'upgrade-insecure-requests': '1', 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36', accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'en-US,en;q=0.9,ru;q=0.8', 'if-none-match': 'W/"40e-JviTST4QyiABJz2Lg+QxzZtiXv8"' } 'accept-language': 'en-US,en;q=0.9,ru;q=0.8',
  35. 35. Wrapper Service @Injectable() export class WindowService { private _window: Window; constructor(@Inject(PLATFORM_ID) platformId: any, @Optional @Inject(REQUEST) private request: any ) { if (isPlatformServer(platformId)) { this._window = { navigator: { language: this.request.headers['accept-language'] }, URL: this.request.headers.host + '' + this.request.url }; } else { this._window = window; } } get nativeWindow(): any { return this._window; } }
  36. 36. Wrapper Service import { Component , OnInit, Inject} from '@angular/core'; import { WINDOW } from '@ng-toolkit/universal'; export class AppComponent implements OnInit { constructor(@Inject(WINDOW) private window: Window) {} public ngOnInit(): void { console.log(window.navigator.language); } } app.component.ts console.log(this.window.navigator.language);
  37. 37. Wrapper Service import { NgtUniversalModule } from '@ng-toolkit/universal'; import { NgModule } from '@angular/core'; @NgModule({ imports:[ NgtUniversalModule ] }) export class AppModule { } app.module.ts
  38. 38. Server/Browser specific code @ngx-translate • i18n module • multiple ways of usage {{‘Welcome to' | translate}} <div [innerHTML]="'HELLO' | translate"></div> { "Welcome to": "Ласкаво просимо в" } uk.json
  39. 39. Server/Browser specific elements import { TranslateHttpLoader } from '@ngx-translate/http-loader'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; export function HttpLoaderFactory(http: HttpClient) { return new TranslateHttpLoader(http); } @NgModule({ imports:[ TranslateModule.forRoot({ loader: {provide: TranslateLoader, useFactory: HttpLoaderFactory, deps: [httpClient]} }) ] }) export class AppBrowserModule {} export function httpLoaderFactory(http: HttpClient): TranslateLoader { return new TranslateHttpLoader(http); } HttpLoaderFactory, app.browser.module.ts
  40. 40. Server/Browser specific elements import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { Observable, Observer } from 'rxjs'; import * as fs from 'fs'; export function universalLoader(): TranslateLoader { return { getTranslation: (lang: string) => { return Observable.create((observer: Observer<any>) => { observer.next(JSON.parse(fs.readFileSync(`./dist/assets/i18n/${lang}.json`, 'utf8'))); observer.complete(); }); } } as TranslateLoader; } @NgModule({ imports:[ TranslateModule.forRoot({ loader: {provide: TranslateLoader, useFactory: universalLoader} }) ] }) export class AppServerModule {} export function universalLoader(): TranslateLoader { app.server.module.ts universalLoader }
  41. 41. Server/Browser specific elements import { Component, OnInit, Inject } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { WINDOW } from '@ng-toolkit/universal'; @Component({ selector: 'app-root', templateUrl: './app.component.html', }) export class AppComponent implements OnInit { constructor( @Inject(WINDOW) private window: Window, private translateService: TranslateService ) {} public ngOnInit(): void { this.translateService.use(this.window.navigator.language); } } app.component.ts this.translateService.use(this.window.navigator.language);
  42. 42. Server/Browser specific elements https://www.twilio.com/blog/create-search-engine-friendly-internationalized-web-apps- angular-universal-ngx-translate
  43. 43. Outline • Why SPAs are not SEO? • Server-side rendering concept • Working with server-side and browser-side code • API calls performance • What’s more? & Deployment • Prerendering & Summary
  44. 44. DRY(c) Don’t repeat your calls export class AppComponent implements OnInit { public post: Observable<any>; constructor(private httpClient: HttpClient) {} public ngOnInit(): void { this.post = this.httpClient.get('https://jsonplaceholder.typicode.com/posts/1'); } }
  45. 45. 2 1 3 4 5 6 external.api.com
  46. 46. external.api.com my-website.com DRY(c) Don’t repeat your calls 1 2 3
  47. 47. HttpCacheModule npm install @nguniversal/common import { NgtUniversalModule } from '@ng-toolkit/universal'; import { CommonModule } from '@angular/common'; import { HttpClientModule } from '@angular/common/http'; import { TransferHttpCacheModule } from '@nguniversal/common'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], imports:[ CommonModule, NgtUniversalModule, TransferHttpCacheModule, HttpClientModule ] }) export class AppModule { } TransferHttpCacheModule
  48. 48. TransferState • ServerTransferStateModule (@angular/platform-server) • BrowserTransferStateModule (@angular/platform-browser) • get(key, fallbackValue) • set(key, value) • has(key) • remove(key)
  49. 49. HTTP_INTERCEPTOR • Provided in the AppModule • Every http request made with HttpClient goes threw it • Used to transform request or response ie: • Adding authentication headers
  50. 50. HTTP_INTERCEPTOR @Injectable() export class ServerStateInterceptor implements HttpInterceptor {     constructor(private _transferState: TransferState) {}     intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {         return next.handle(req).pipe(tap(event => {             if (event instanceof HttpResponse) {                 this._transferState.set(makeStateKey(req.url), event.body);             }         }));     } }
  51. 51. HTTP_INTERCEPTOR @Injectable() export class BrowserStateInterceptor implements HttpInterceptor {     constructor(private _transferState: TransferState) { }     intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {         if (req.method !== 'GET') {             return next.handle(req);         }         const storedResponse: string = this._transferState.get(makeStateKey(req.url), null);         if (storedResponse) {             const response = new HttpResponse({ body: storedResponse, status: 200 }); this._transferState.remove(makeStateKey(req.url));             return of(response);         }         return next.handle(req);     } }
  52. 52. HTTP_INTERCEPTOR import {HTTP_INTERCEPTORS } from '@angular/common/http'; providers: [ { provide: HTTP_INTERCEPTORS, useClass: BrowserStateInterceptor, multi: true, } ] import {HTTP_INTERCEPTORS } from '@angular/common/http'; providers: [ { provide: HTTP_INTERCEPTORS, useClass: ServerStateInterceptor, multi: true, } ]
  53. 53. Performance export class RouteResolverService implements Resolve<any> { constructor( private httpClient: HttpClient, @Inject(PLATFORM_ID) private platformId: any ) {} public resolve(): Observable<any> { } } const watchdog: Observable<number> = timer(500); if (isPlatformBrowser(this.platformId)) { return this.httpClient.get<any>('https://jsonplaceholder.typicode.com/posts/1'); } return Observable.create(subject => { this.httpClient.get<any>('https://jsonplaceholder.typicode.com/posts/1') .subscribe(response => { subject.next(response); subject.complete(); }); }) .pipe(takeUntil(watchdog)) watchdog.subscribe(() => { subject.next('timeout'); subject.complete() })
  54. 54. 0.5sec
  55. 55. 0.5sec
  56. 56. DRY(c) & Performance https://www.twilio.com/blog/faster-javascript-web-apps-angular-universal-transferstate-api-watchdog
  57. 57. Outline • Why SPAs are not SEO? • Server-side rendering concept • Working with server-side and browser-side code • API calls performance • What’s more? & Deployment • Prerendering & Summary
  58. 58. It’s not only SSR const listFiles = (callBack) => { return fs.readdir('./user_upload', callBack); }; app.engine('html', ngExpressEngine({ bootstrap: AppServerModuleNgFactory, providers: [ provideModuleMap(LAZY_MODULE_MAP), {provide: 'LIST_FILES', useValue: listFiles} ] })); constructor( private http: HttpClient, @Optional() @Inject('LIST_FILES') private listFiles: (callback) => void, @Inject(PLATFORM_ID) private platformId: any, private transferState: TransferState ) { const transferKey: StateKey<string> = makeStateKey<string>('fileList'); if (isPlatformServer(this.platformId)) { this.listFiles((err, files) => { this.fileList = files; this.transferState.set(transferKey, this.fileList); }); } else { this.fileList = this.transferState.get<string[]>(transferKey, []); } } server.ts someService.ts
  59. 59. It’s not only SSR https://www.twilio.com/blog/transfer-files-data-javascript-applications-angular-node-js
  60. 60. Let’s go Serverless! • Function as a Service • Event-driven • Scalable • Pay for the up-time
  61. 61. Let’s go Serverless! https://www.twilio.com/blog/angular-universal-javascript-node-js-aws-lambda
  62. 62. Outline • Why SPAs are not SEO? • Server-side rendering concept • Working with server-side and browser-side code • API calls performance • What’s more? & Deployment • Prerendering & Summary
  63. 63. Prerender • Generating HTML files at a build time • Can be hosted from traditional hosting (ie. AWS S3) • Doesn’t perform dynamic request • https://github.com/maciejtreder/angular-ssr-prerender Alternative
  64. 64. Summary server-side renderingprerenderng build —prod SEO Performance Difficulty SEO + external calls Additional back-end logic
  65. 65. @maciejtreder

×