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:
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.
*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.
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.
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.
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.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