Angular Server Side Rendering State Transfer For HTTP Requests

Written by printfmyname | Published 2020/10/07
Tech Story Tags: angular | server-side-rendering | web-development | seo-optimization | pagespeed | caching | angular-development | angular-application

TLDR This tutorial extends the SSR explained on Server-side rendering (SSR) with Angular Universal page. Our goal is to store data received from SSR and reuse them without making any network requests. We create a HttpInterceptor to intercept SSR HTP requests and store results on TransferState service. This data store is serialized and passed to the client-side with the rendered page. All the requests you do will go through this interceptor. On the above example, I save all successful HttpResponses on to the state store.via the TL;DR App

This tutorial extends the SSR explained on Server-side rendering (SSR) with Angular Universal page. This tutorial fixes the content flash occurs on SSR write after page loads due to content refresh caused by data received through network requests. If you are crazy about PageSpeed/Web Vitals score as much as me, this will help you to improve:
  1. Largest Contentful Paint (LCP)
  2. First Input Delay (FID)
  3. Cumulative Layout Shift (CLS)
I have tested this on Angular 9 and 10.
Before continuing, please make sure you have SSR set up as mentioned on angular.io. Our goal is to store data received from SSR and reuse them without making any network requests.
  1. Create a HttpInterceptor to intercept SSR HTP requests and store results on TransferState service
  2. Add interceptor to the app.server.module.ts*
  3. Create a HttpInterceptor to intercept HTP requests happen on the client-side and return result from state service instead of making a network request
  4. Add interceptor to the app.module.ts*
*app.module is the module where you put modules required to render things on the browser. app.server.module is where you put things required for the rendering page on the server.

Step 1: Interceptor for SSR to cache HTTP requests

import { HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { makeStateKey, TransferState } from '@angular/platform-browser';
import { tap } from 'rxjs/operators';

@Injectable()
export class ServerStateInterceptor implements HttpInterceptor {

    constructor(private transferState: TransferState) { }

    intercept(req: HttpRequest<any>, next: HttpHandler) {
        return next.handle(req).pipe(
            tap(event => {
                if ((event instanceof HttpResponse && (event.status === 200 || event.status === 202))) {
                    this.transferState.set(makeStateKey(req.url), event.body);
                }
            }),
        );
    }
}
Here the transferState is the service that has the data store. This data store is serialized and passed to the client-side with the rendered page.
intercept()
is the method we have to implement from HttpInterceptor interface.
Angular renderer waits for your asynchronous task to be executed before generating the HTML page. After we add this interceptor to the server module, all the HTTP requests you do will go through this interceptor. On the above example, I save all the successful HttpResponses on to the state store.

Step 2: Register it with your server module

import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { ServerModule, ServerTransferStateModule } from '@angular/platform-server';

import { AppComponent } from './app.component';
import { AppModule } from './app.module';
import { ServerStateInterceptor } from './serverstate.interceptor';

@NgModule({
    imports: [
        AppModule,
        ServerModule,
        ServerTransferStateModule,

    ],
    providers: [
        // Add universal-only providers here
        {
            provide: HTTP_INTERCEPTORS,
            useClass: ServerStateInterceptor,
            multi: true
        }
    ],
    bootstrap: [AppComponent],
})
export class AppServerModule { }
This register the ServerStateInterceptor with the universal app.

Step 3: Create a browser interceptor that will intercepts client-side requests and return data from the cache

import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { makeStateKey, TransferState } from '@angular/platform-browser';
import { Observable, of } from 'rxjs';

@Injectable({
    providedIn: 'root'
})
export class BrowserStateInterceptor implements HttpInterceptor {

    constructor(
        private transferState: TransferState,
    ) { }

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        if (req.method === 'GET') {
            const key = makeStateKey(req.url);
            const storedResponse: string = this.transferState.get(key, null);
            if (storedResponse) {
                const response = new HttpResponse({ body: storedResponse, status: 200 });
                return of(response);
            }
        }

        return next.handle(req);
    }
}
On my app, I only want to cache the HTTP GET requests, hence the if
(req.method === 'GET') {
. This is pretty simple, once this interceptor is registered with the app.module, whenever we make a request, this interceptor checks if our transferState store has a value for the request URL and if it does it returns the cached value, else it let the Angular HTTP client handle the request.

Step 4: Add interceptor to the app.module.ts

import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { BrowserModule, BrowserTransferStateModule } from '@angular/platform-browser';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule } from '@angular/router';
import { TransferHttpCacheModule } from '@nguniversal/common';

import { AppComponent } from './app.component';
import { appRoutes } from './app.route';
import { BrowserStateInterceptor } from './browserstate.interceptor';

@NgModule({
  declarations: [
    AppComponent,
  ],
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: BrowserStateInterceptor,
      multi: true
    },
  ],
  imports: [
    HttpClientModule,
    BrowserModule.withServerTransition({ appId: 'my-app', }),
    NoopAnimationsModule,
    BrowserTransferStateModule,
    RouterModule.forRoot(appRoutes,
      {
        enableTracing: false,
        initialNavigation: 'enabled',
      },
    ),
    ReactiveFormsModule,
    TransferHttpCacheModule,
  ],
  exports: [

  ],
  bootstrap: [AppComponent],
})
export class AppModule {
}
An important thing to mention on app.module is to set initialNavigation: 'enabled', because
"When set to enabled, the initial navigation starts before the root component is created. The bootstrap is blocked until the initial navigation is complete. This value is required for server-side rendering to work. When set to disabled, the initial navigation is not performed. The location listener is set up before the root component gets created. Use if there is a reason to have more control over when the router starts its initial navigation due to some complex initialization logic."
- Angular Team
That is the first thing you need to prevent a major flicker or flash screen on an SSR app.
Why is this important?
On March, 4th there was a Google updated their search engine algo from which I got hit bad. I mean 70% reduction in one day. I started finding clues about what led to this issue. Not knowing the exact answer, I started and continue to improve my website speed and internal SEO.
I am using this same implementation to improve speed on my video compressor page. It almost hits a 90 score on PageSpeed after the improvements.
There were other improvements such as LazyLoading routes and reduce the use of getters on HTML templates, but what helped the most is to caching HTTP responses.
I will write follow up article on a way to reduce TTFB and FCP by caching the rendered page on the server to avoid rendering multiple times.
If you like to know about any technologies we use or have a suggestion for another article, please comment here.
Cover photo credit: Photo by Marc-Olivier Jodoin on Unsplash

Published by HackerNoon on 2020/10/07