angularserver-side-renderingangular-universalfouc

Angular universal styling FOUC on server to client transition


I have an issue with my universal app, where when the server side app is swapped for the client side app, there is a moment when there is no styling for the components which are part of my routed component for that page. This results in the page loading correctly, then momentarily displaying a flash of unstyled content (FOUC) and looking horrible, before sorting itself out.

The styling for my website header and footer components look fine the whole time, but the components that load inside the <router-outlet> element are the ones that do not have the correct styling.

I am using Preboot to manage the server > client transition and not doing anything outside of the standard configuration. I have experimented using @ngx-universal/state-transfer and @ngx-cache libraries, but I don't think they are what I need.

I am using lazy loaded routes, but I have experimented with removing these and the error is the same. I have also tried setting { initialNavigation: 'enabled' } in my routing config.

I use webpack to build my server side app, and the Angular CLI for the client side one, mostly based on this project, and I use the AOT compiler. Any ideas would be very appreciated, thanks!


Solution

  • So the solution I ended up going with here was to create a StyleStateTransferService which utilises the @ngx-universal/state-transfer library.

    In the server side app, I pull the angular styles out of the head and add them into the state transfer service. In the client side app I get the styles out of the transferred state and add them into the head. Once Angular has finished bootstrapping I then go through and remove the ones I added in, just so there's no duplicates hanging around.

    I don't know if it is the best solution, maybe someone has a better one, I was initially hoping there was just some configuration I was missing from preboot, but it doesn't seem so.

    Service:

    @Injectable()
    export class StyleStateTransferService {
      document: any;
      window: Window;
      renderer: Renderer2;
      ngStyleId = 'ng_styles';
    
      constructor(private stateTransferService: StateTransferService,
                  @Inject(DOCUMENT) document: any,
                  private winRef: WindowRef,
                  private rendererFactory: RendererFactory2) {
        this.window = winRef.nativeWindow;
        this.document = document;
        this.renderer = rendererFactory.createRenderer(this.document, null);
      }
    
      addStylesToState() {
        const styles: string[] = this.document.head.children
          // elements have a weird structure on the server
          // this filters to style tags with the ng-transition attribute that have content
          .filter(el =>
            el.name === 'style' && el.attribs['ng-transition'] && el.firstChild && el.firstChild.data)
          // extract the css content of the style tags
          .map(el => el.firstChild.data.replace(/\n/g, ' '));
    
        this.stateTransferService.set(this.ngStyleId, styles);
        this.stateTransferService.inject();
      }
    
      injectStylesFromState() {
        const styles = _.get(this.window, `${DEFAULT_STATE_ID}.${this.ngStyleId}`, []);
        styles.forEach(content => {
          const styleEl = this.renderer.createElement('style');
          // set this attribute so we can remove them later
          this.renderer.setAttribute(styleEl, 'ng-state-transfer', null);
          this.renderer.setProperty(styleEl, 'innerHTML', content);
          this.renderer.appendChild(this.document.head, styleEl);
        });
      }
    
      cleanupInjectedStyles() {
        Array.from(<HTMLElement[]>this.document.head.children)
          .filter(htmlEl => htmlEl.tagName === 'STYLE' && htmlEl.hasAttribute('ng-state-transfer'))
          .forEach(styleEl => this.renderer.removeChild(this.document.head, styleEl));
    }
    

    Using it in server and browser modules (clean up happens in app component but didn't seem worth showing):

    export class AppServerModule {
      constructor(private appRef: ApplicationRef,
                  private styleStateTransferService: StyleStateTransferService) {
        // waits until the app has fully initialised before adding the styles to the state
        this.appRef.isStable
          .filter(isStable => isStable)
          .first()
          .subscribe(() => {
            this.styleStateTransferService.addStylesToState();
          });
      }
    }
    
    export class AppBrowserModule {
      constructor(private styleStateTransferService: StyleStateTransferService) {
        this.styleStateTransferService.injectStylesFromState();
      }
    }