Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular PWA update is too slow

I have an Angular 8 PWA app in production. I make a lot of updates regularly, so I need to find a way for users to get those updates when they open the app.

Without special action, the app won't update. If you open the app on your browser, it will show the version from when you last opened the app, even if I have pushed a new bundle to production. It seems this is an unfortunate result of PWA-functionality.

I am hosting with AWS Amplify.

To solve this (I had asked a question about it here), I have tried using swUpdate.

The problem is it works way too slow. It will reload the browser if there is an update--but it takes a while to do that. It regularly takes several seconds after a user opens the app for the reload to happen. So you open the app, and 4 to 6 seconds later the app reloads with the new version.

Is there a way to make swUpdate go faster? Or an alternative way to load the new app version?

Here's my code, which I think is a straightforward implementation:

app.component.ts:

import { Component } from '@angular/core';
import { SwUpdate } from '@angular/service-worker';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {

   constructor(private swUpdate: SwUpdate) {
   title = 'Great App'

   swUpdate.available.subscribe(event => {
        swUpdate.activateUpdate().then(() => {
            window.location.reload();
            console.log('there is an Update! Reloading now.')
      });
    })

   if (!this.swUpdate.isEnabled) {
      console.log('Not going to update');
    }
 }

But this is not working well, because the reload often happens several seconds after the user goes to the app (even on good internet connection).

I know that I could also show a message to people saying "Want to refresh with a new version?" But this would not address the underlying problem of how slow swUpdate currently works.

like image 575
9gt53wS Avatar asked Apr 05 '20 12:04

9gt53wS


2 Answers

To anyone still having this issue, here is what I did to reduce the delay:

  • Check for updates in an APP_INITIALIZER instead of the AppComponent, so that the check is done earlier
  • Use the checkForUpdate method

In your app.module.ts add an initializer function:

{ provide: APP_INITIALIZER, useFactory: checkForUpdates, multi: true, deps: [SwUpdate /* Add whatever dependency is required here */] },

This function should look like this:

export const checkForUpdates = (swUpdate: SwUpdate): (() => Promise<any>) => {
  return (): Promise<void> =>
    new Promise((resolve) => {
      swUpdate.checkForUpdate();
      
      swUpdate.available.subscribe(() => {
          window.location.reload();
      });

      resolve();
    });
};

I'm not sure if both APP_INITIALIZER and checkForUpdate are required, but this way I reduced the delay to 1-2s (instead of 5-10s or even more)

like image 156
Carrm Avatar answered Nov 16 '22 09:11

Carrm


Problem

There are 4 different registration strategies for a Service Worker in Angular, which determines when it will be registered with the browser.

  1. registerWhenStable:<timeout>: Register as soon as the application stabilizes (no pending micro-/macro-tasks) but no later than milliseconds. If the app hasn't stabilized after milliseconds (for example, due to a recurrent asynchronous task), the ServiceWorker will be registered anyway. If is omitted, the ServiceWorker will only be registered once the app stabilizes.
  2. registerImmediately: Register immediately.
  3. registerWithDelay:<timeout>: Register with a delay of milliseconds. For example, use registerWithDelay:5000 to register the ServiceWorker after 5 seconds. If is omitted, is defaults to 0, which will register the ServiceWorker as soon as possible but still asynchronously, once all pending micro-tasks are completed.
  4. An Observable factory function: A function that returns an Observable. The function will be used at runtime to obtain and subscribe to the Observable and the ServiceWorker will be registered as soon as the first value is emitted.

NOTE: Angular is using registerWhenStable:30000 as default. That means that it will first wait for application to stabilizes and then it will register a Service Worker (or it will register the Service Worker after 30sec if the application does not stabilize until then).

Solution

Instead of default registerWhenStable:30000, you can set registerImmediately strategy. Then you can for example add APP_INITIALIZER and inside it check if there is a new version and load it. In that case, user will not see the old version if a new version is available.

app.module.ts

import { APP_INITIALIZER } from '@angular/core';
import { ServiceWorkerModule, SwUpdate } from '@angular/service-worker';
...
function initializeApp(): Promise<any> {
  return new Promise(async (resolve, reject) => {
    try {
      // Check if Service Worker is supported by the Browser
      if (this.swUpdate.isEnabled) {
        const isNewVersion = await this.swUpdate.checkForUpdate();
        // Check if the new version is available
        if (isNewVersion) {
          const isNewVersionActivated = await this.swUpdate.activateUpdate();
          // Check if the new version is activated and reload the app if it is
          if (isNewVersionActivated) window.location.reload();
          resolve(true);
        }
        resolve(true);
      }
      resolve(true);
    } catch (error) {
      window.location.reload();
    }
  });
}

...

@NgModule({
  ...
  imports: [
    ...,
    ServiceWorkerModule.register('ngsw-worker.js', {
      enabled: environment.production,
      registrationStrategy: 'registerImmediately',
    }),
  ],
  providers: [
    ...,
    { provide: APP_INITIALIZER, useFactory: initializeApp, deps: [SwUpdate], multi: true },
  ],
})
export class AppModule {}
like image 39
NeNaD Avatar answered Nov 16 '22 09:11

NeNaD