KEMBAR78
Angular server side rendering - Strategies & Technics | PDF
SSR (Server Side Rendering)
Strategies & Technics
About mySelf
• Experienced FE developer, specialised in B2C applications.
• FE Team Lead @ market.co.uk
• Weekends FE developer @ fashbash.co
Server Side Rendering
Server Side Rendering
What is it?
SSR Will give you the ability
to run and serve your app
from the server
Or in other words…
Pre-Rendering
Angular is SPA
And thats a huge
advantage
But…
There is a cost
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>ServerSideRenderingTalk</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<app-root></app-root>
</body>
</html>
Angular
Universal
Fast
Loading
SEO
Crawlable Scrapable
Fast loading
"We found that 53% of mobile site visits
are abandoned if pages take longer than
3 second to load…”
• - - prod (AOT, Minification, tree shacking)
• Lazy Loading
• Soon to by - Ivy
• Opportunity for improve?
• Time between html load & bootstrap
Page load Optimisations
What we want to measure?
Time for first
meaningful paint
Time to interactive
Page Load
Page Load
SSR
Regular

load
0 30 60 90 120
index.html load App bootstrap First view
Social
Scrapeable
SEO crawlers
They can’t run
JavaScript
And yet…
Universal
can
be a heavy lift
• Unsupported features:
• window object
• document
• localStorage
• nativeElement changed directly
• And More…
Not everything is PINK
Do I really need
this?
“Do new users discover
your app for the first
time through a link”
- Jeff Whelpley, co-founder of Angular Universal
If you need it,
Let’s do it!
Demo…
ng generate universal --client-project *project-name*
CREATE src/main.server.ts (220 bytes)
CREATE src/app/app.server.module.ts (318 bytes)
CREATE src/tsconfig.server.json (219 bytes)
UPDATE package.json (1374 bytes)
UPDATE angular.json (4107 bytes)
UPDATE src/main.ts (430 bytes)
UPDATE src/app/app.module.ts (965 bytes)
renderModuleFactory(AppServerModuleNgFactory, {
document: index,
url: req.url
})
npm install @nguniversal/express-engine --save
npm install @nguniversal/module-map-ngfactory-
loader --save
"server-build": "ng build --prod && ng run server-side-
rendering-talk:server”
"start:server": "./node_modules/.bin/ts-node ./
server.ts"
package.json
enableProdMode();
const app = express();
const distPath = __dirname + '/dist';
app.engine('html', ngExpressEngine({
bootstrap: AppServerModuleNgFactory,
providers: [provideModuleMap(LAZY_MODULE_MAP)]
}));
app.set('view engine', 'html');
app.set('views', distPath);
app.get('*.*', express.static(distPath));
app.get('*', (req, res) => {
res.render('index', {req});
});
server.ts
enableProdMode();
const app = express();
const distPath = __dirname + '/dist';
app.engine('html', ngExpressEngine({
bootstrap: AppServerModuleNgFactory,
providers: [provideModuleMap(LAZY_MODULE_MAP)]
}));
app.set('view engine', 'html');
app.set('views', distPath);
app.get('*.*', express.static(distPath));
app.get('*', (req, res) => {
res.render('index', {req});
});
server.ts
enableProdMode();
const app = express();
const distPath = __dirname + '/dist';
app.engine('html', ngExpressEngine({
bootstrap: AppServerModuleNgFactory,
providers: [provideModuleMap(LAZY_MODULE_MAP)]
}));
app.set('view engine', 'html');
app.set('views', distPath);
app.get('*.*', express.static(distPath));
app.get('*', (req, res) => {
res.render('index', {req});
});
server.ts
enableProdMode();
const app = express();
const distPath = __dirname + '/dist';
app.engine('html', ngExpressEngine({
bootstrap: AppServerModuleNgFactory,
providers: [provideModuleMap(LAZY_MODULE_MAP)]
}));
app.set('view engine', 'html');
app.set('views', distPath);
app.get('*.*', express.static(distPath));
app.get('*', (req, res) => {
res.render('index', {req});
});
server.ts
enableProdMode();
const app = express();
const distPath = __dirname + '/dist';
app.engine('html', ngExpressEngine({
bootstrap: AppServerModuleNgFactory,
providers: [provideModuleMap(LAZY_MODULE_MAP)]
}));
app.set('view engine', 'html');
app.set('views', distPath);
app.get('*.*', express.static(distPath));
app.get('*', (req, res) => {
res.render('index', {req});
});
server.ts
Control the META
ngOnInit() {
this.paramsSubscription = this.route.data.subscribe((data: any) => {
this.item = data['data'][0];
this.title.setTitle(this.item.description);
this.meta.addTag({
name: 'description',
content: this.item.description
});
});
product-page.component.ts
const twitterTags = [];
twitterTags.push({ name: 'twitter:card', content: 'summary' });
twitterTags.push({ name: 'twitter:site', content: '@EliranEliassy' });
twitterTags.push({ name: 'twitter:title', content: this.item.description });
twitterTags.push({ name: 'twitter:description', content: this.item.description });
twitterTags.push({ name: 'twitter:text:description', content: this.item.description });
twitterTags.push({ name: 'twitter:image', content: this.item.imageUrl });
this.meta.addTags(twitterTags);
product-page.component.ts
const fbTags = [];
fbTags.push({ name: 'og:title', content: this.item.description });
fbTags.push({ name: 'og:image', content: this.item.imageUrl });
fbTags.push({ name: 'og:site_name', content: 'Fake MarketPlace' });
fbTags.push({ name: 'og:description', content: this.item.description });
this.meta.addTags(fbTags);
product-page.component.ts
NOW…Let’s check
the performance
Client Side Rendering
Server Side Rendering
• Time for first meaningful paint: CSR < SSR
• Time to interactive: CSR > SSR
Results
You downloading a lot of text
with your index.html
What can we do?
1. Partially SSR
SsrNoRender Directive
constructor(private viewContainerRef: ViewContainerRef,
private templateRef: TemplateRef<any>,
@Inject(PLATFORM_ID) private platformId) { }
ngOnInit(): void {
if (isPlatformServer(this.platformId)) {
this.viewContainerRef.clear();
} else {
this.viewContainerRef.createEmbeddedView(this.templateRef);
}
}
SsrNoRender Directive
constructor(private viewContainerRef: ViewContainerRef,
private templateRef: TemplateRef<any>,
@Inject(PLATFORM_ID) private platformId) { }
ngOnInit(): void {
if (isPlatformServer(this.platformId)) {
this.viewContainerRef.clear();
} else {
this.viewContainerRef.createEmbeddedView(this.templateRef);
}
}
app.component.html
<app-header></app-header>
<div class="container text-center">
<ng-container *appSsrNoRender>
<router-outlet></router-outlet>
</ng-container>
<div class="loader" *appSsrRender></div>
</div>
2. Angular
Elements
3. Avoid double
XHR requests
Product page resolver
resolve(route: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<any> {
const productId = route.params['id'];
return this.productService.getProductById(productId);
}
Product page resolver
constructor(private productService: ProductService,
@Inject(PLATFORM_ID) private platformId,
private transferState: TransferState) { }
Product page resolver
const productId = route.params['id'];
const key = makeStateKey<any>(productId);
if (this.transferState.hasKey(key)) {
const item = this.transferState.get<any>(key, null);
this.transferState.remove(key);
return of(item);
} else {
return this.productService.getProductById(productId)
.pipe(
tap(item => {
if (isPlatformServer(this.platformId)) {
this.transferState.set(key, item);
}
})
);
}
Product page resolver
const productId = route.params['id'];
const key = makeStateKey<any>(productId);
if (this.transferState.hasKey(key)) {
const item = this.transferState.get<any>(key, null);
this.transferState.remove(key);
return of(item);
} else {
return this.productService.getProductById(productId)
.pipe(
tap(item => {
if (isPlatformServer(this.platformId)) {
this.transferState.set(key, item);
}
})
);
}
Product page resolver
const productId = route.params['id'];
const key = makeStateKey<any>(productId);
if (this.transferState.hasKey(key)) {
const item = this.transferState.get<any>(key, null);
this.transferState.remove(key);
return of(item);
} else {
return this.productService.getProductById(productId)
.pipe(
tap(item => {
if (isPlatformServer(this.platformId)) {
this.transferState.set(key, item);
}
})
);
}
4. Server-Cache
your response
const cache = new Map();
export function ngExpressEngine(setupOptions) {
return function (filePath, options, callback) {
if (!cache.has(filePath)) {
const content = fs.readFileSync(filePath).toString();
cache.set(filePath, content);
}
renderModuleFactory(setupOptions.bootstrap[0], {
document: cache.get(filePath),
url: options.req.url
})
.then(string => {
callback(null, string);
});
};
}
server.ts
const cache = new Map();
export function ngExpressEngine(setupOptions) {
return function (filePath, options, callback) {
if (!cache.has(filePath)) {
const content = fs.readFileSync(filePath).toString();
cache.set(filePath, content);
}
renderModuleFactory(setupOptions.bootstrap[0], {
document: cache.get(filePath),
url: options.req.url
})
.then(string => {
callback(null, string);
});
};
}
server.ts
Did I said
that it can
be a
heavy lift?
Deployment
Run it on the cloud
• Super FAST response time
• Cheaper than servers instances
• Easy to deploy
Thank You
@EliranEliassy eliran.eliassy@gmail.comeliraneliassy

Angular server side rendering - Strategies & Technics

  • 1.
    SSR (Server SideRendering) Strategies & Technics
  • 2.
    About mySelf • ExperiencedFE developer, specialised in B2C applications. • FE Team Lead @ market.co.uk • Weekends FE developer @ fashbash.co
  • 5.
  • 6.
  • 7.
  • 8.
    SSR Will giveyou the ability to run and serve your app from the server
  • 9.
    Or in otherwords… Pre-Rendering
  • 10.
  • 11.
    And thats ahuge advantage But… There is a cost
  • 12.
    <!doctype html> <html lang="en"> <head> <metacharset="utf-8"> <title>ServerSideRenderingTalk</title> <base href="/"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" type="image/x-icon" href="favicon.ico"> </head> <body> <app-root></app-root> </body> </html>
  • 13.
  • 14.
  • 15.
    "We found that53% of mobile site visits are abandoned if pages take longer than 3 second to load…”
  • 16.
    • - -prod (AOT, Minification, tree shacking) • Lazy Loading • Soon to by - Ivy • Opportunity for improve? • Time between html load & bootstrap Page load Optimisations
  • 17.
    What we wantto measure? Time for first meaningful paint Time to interactive
  • 18.
  • 19.
    Page Load SSR Regular
 load 0 3060 90 120 index.html load App bootstrap First view
  • 20.
  • 24.
  • 26.
  • 27.
  • 28.
    • Unsupported features: •window object • document • localStorage • nativeElement changed directly • And More… Not everything is PINK
  • 29.
    Do I reallyneed this?
  • 30.
    “Do new usersdiscover your app for the first time through a link” - Jeff Whelpley, co-founder of Angular Universal
  • 31.
    If you needit, Let’s do it! Demo…
  • 32.
    ng generate universal--client-project *project-name* CREATE src/main.server.ts (220 bytes) CREATE src/app/app.server.module.ts (318 bytes) CREATE src/tsconfig.server.json (219 bytes) UPDATE package.json (1374 bytes) UPDATE angular.json (4107 bytes) UPDATE src/main.ts (430 bytes) UPDATE src/app/app.module.ts (965 bytes)
  • 33.
  • 34.
    npm install @nguniversal/express-engine--save npm install @nguniversal/module-map-ngfactory- loader --save
  • 35.
    "server-build": "ng build--prod && ng run server-side- rendering-talk:server” "start:server": "./node_modules/.bin/ts-node ./ server.ts" package.json
  • 36.
    enableProdMode(); const app =express(); const distPath = __dirname + '/dist'; app.engine('html', ngExpressEngine({ bootstrap: AppServerModuleNgFactory, providers: [provideModuleMap(LAZY_MODULE_MAP)] })); app.set('view engine', 'html'); app.set('views', distPath); app.get('*.*', express.static(distPath)); app.get('*', (req, res) => { res.render('index', {req}); }); server.ts
  • 37.
    enableProdMode(); const app =express(); const distPath = __dirname + '/dist'; app.engine('html', ngExpressEngine({ bootstrap: AppServerModuleNgFactory, providers: [provideModuleMap(LAZY_MODULE_MAP)] })); app.set('view engine', 'html'); app.set('views', distPath); app.get('*.*', express.static(distPath)); app.get('*', (req, res) => { res.render('index', {req}); }); server.ts
  • 38.
    enableProdMode(); const app =express(); const distPath = __dirname + '/dist'; app.engine('html', ngExpressEngine({ bootstrap: AppServerModuleNgFactory, providers: [provideModuleMap(LAZY_MODULE_MAP)] })); app.set('view engine', 'html'); app.set('views', distPath); app.get('*.*', express.static(distPath)); app.get('*', (req, res) => { res.render('index', {req}); }); server.ts
  • 39.
    enableProdMode(); const app =express(); const distPath = __dirname + '/dist'; app.engine('html', ngExpressEngine({ bootstrap: AppServerModuleNgFactory, providers: [provideModuleMap(LAZY_MODULE_MAP)] })); app.set('view engine', 'html'); app.set('views', distPath); app.get('*.*', express.static(distPath)); app.get('*', (req, res) => { res.render('index', {req}); }); server.ts
  • 40.
    enableProdMode(); const app =express(); const distPath = __dirname + '/dist'; app.engine('html', ngExpressEngine({ bootstrap: AppServerModuleNgFactory, providers: [provideModuleMap(LAZY_MODULE_MAP)] })); app.set('view engine', 'html'); app.set('views', distPath); app.get('*.*', express.static(distPath)); app.get('*', (req, res) => { res.render('index', {req}); }); server.ts
  • 41.
  • 42.
    ngOnInit() { this.paramsSubscription =this.route.data.subscribe((data: any) => { this.item = data['data'][0]; this.title.setTitle(this.item.description); this.meta.addTag({ name: 'description', content: this.item.description }); }); product-page.component.ts
  • 43.
    const twitterTags =[]; twitterTags.push({ name: 'twitter:card', content: 'summary' }); twitterTags.push({ name: 'twitter:site', content: '@EliranEliassy' }); twitterTags.push({ name: 'twitter:title', content: this.item.description }); twitterTags.push({ name: 'twitter:description', content: this.item.description }); twitterTags.push({ name: 'twitter:text:description', content: this.item.description }); twitterTags.push({ name: 'twitter:image', content: this.item.imageUrl }); this.meta.addTags(twitterTags); product-page.component.ts
  • 44.
    const fbTags =[]; fbTags.push({ name: 'og:title', content: this.item.description }); fbTags.push({ name: 'og:image', content: this.item.imageUrl }); fbTags.push({ name: 'og:site_name', content: 'Fake MarketPlace' }); fbTags.push({ name: 'og:description', content: this.item.description }); this.meta.addTags(fbTags); product-page.component.ts
  • 45.
  • 46.
  • 47.
  • 48.
    • Time forfirst meaningful paint: CSR < SSR • Time to interactive: CSR > SSR Results You downloading a lot of text with your index.html
  • 50.
  • 51.
  • 52.
    SsrNoRender Directive constructor(private viewContainerRef:ViewContainerRef, private templateRef: TemplateRef<any>, @Inject(PLATFORM_ID) private platformId) { } ngOnInit(): void { if (isPlatformServer(this.platformId)) { this.viewContainerRef.clear(); } else { this.viewContainerRef.createEmbeddedView(this.templateRef); } }
  • 53.
    SsrNoRender Directive constructor(private viewContainerRef:ViewContainerRef, private templateRef: TemplateRef<any>, @Inject(PLATFORM_ID) private platformId) { } ngOnInit(): void { if (isPlatformServer(this.platformId)) { this.viewContainerRef.clear(); } else { this.viewContainerRef.createEmbeddedView(this.templateRef); } }
  • 54.
    app.component.html <app-header></app-header> <div class="container text-center"> <ng-container*appSsrNoRender> <router-outlet></router-outlet> </ng-container> <div class="loader" *appSsrRender></div> </div>
  • 55.
  • 56.
  • 57.
    Product page resolver resolve(route:ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<any> { const productId = route.params['id']; return this.productService.getProductById(productId); }
  • 58.
    Product page resolver constructor(privateproductService: ProductService, @Inject(PLATFORM_ID) private platformId, private transferState: TransferState) { }
  • 59.
    Product page resolver constproductId = route.params['id']; const key = makeStateKey<any>(productId); if (this.transferState.hasKey(key)) { const item = this.transferState.get<any>(key, null); this.transferState.remove(key); return of(item); } else { return this.productService.getProductById(productId) .pipe( tap(item => { if (isPlatformServer(this.platformId)) { this.transferState.set(key, item); } }) ); }
  • 60.
    Product page resolver constproductId = route.params['id']; const key = makeStateKey<any>(productId); if (this.transferState.hasKey(key)) { const item = this.transferState.get<any>(key, null); this.transferState.remove(key); return of(item); } else { return this.productService.getProductById(productId) .pipe( tap(item => { if (isPlatformServer(this.platformId)) { this.transferState.set(key, item); } }) ); }
  • 61.
    Product page resolver constproductId = route.params['id']; const key = makeStateKey<any>(productId); if (this.transferState.hasKey(key)) { const item = this.transferState.get<any>(key, null); this.transferState.remove(key); return of(item); } else { return this.productService.getProductById(productId) .pipe( tap(item => { if (isPlatformServer(this.platformId)) { this.transferState.set(key, item); } }) ); }
  • 62.
  • 63.
    const cache =new Map(); export function ngExpressEngine(setupOptions) { return function (filePath, options, callback) { if (!cache.has(filePath)) { const content = fs.readFileSync(filePath).toString(); cache.set(filePath, content); } renderModuleFactory(setupOptions.bootstrap[0], { document: cache.get(filePath), url: options.req.url }) .then(string => { callback(null, string); }); }; } server.ts
  • 64.
    const cache =new Map(); export function ngExpressEngine(setupOptions) { return function (filePath, options, callback) { if (!cache.has(filePath)) { const content = fs.readFileSync(filePath).toString(); cache.set(filePath, content); } renderModuleFactory(setupOptions.bootstrap[0], { document: cache.get(filePath), url: options.req.url }) .then(string => { callback(null, string); }); }; } server.ts
  • 65.
    Did I said thatit can be a heavy lift?
  • 67.
  • 68.
    Run it onthe cloud • Super FAST response time • Cheaper than servers instances • Easy to deploy
  • 69.