Angular 伺服器端渲染應用一個常見的記憶體洩漏問題

注销發表於2022-05-19

考慮如下的 Angular 程式碼:

import { Injectable, NgZone } from "@angular/core";
import { interval } from "rxjs";

@Injectable()
export class LocationService {
  constructor(ngZone: NgZone) {
    ngZone.runOutsideAngular(() => interval(1000).subscribe(() => {
      ...
    }));
  }
}

這段程式碼不會影響應用程式的穩定性,但是如果應用程式在伺服器上被銷燬,傳遞給訂閱的回撥將繼續被呼叫。 伺服器上應用程式的每次啟動都會以 interval 的形式留下一個 artifact.

這是一個潛在的記憶體洩漏點。

這個記憶體洩漏風險可以透過使用 ngOnDestoroy 鉤子解決。這個鉤子適用於 Component 和 service. 我們需要儲存 interval 返回的訂閱(subscription),並在服務被銷燬時終止它。

退訂 subscription 的技巧有很多,下面是一個例子:

import { Injectable, NgZone, OnDestroy } from "@angular/core";
import { interval, Subscription } from "rxjs";

@Injectable()
export class LocationService implements OnDestroy {
  private subscription: Subscription;

  constructor(ngZone: NgZone) {
    this.subscription = ngZone.runOutsideAngular(() =>
      interval(1000).subscribe(() => {})
    );
  }

  ngOnDestroy(): void {
    this.subscription.unsubscribe();
  }
}

螢幕閃爍問題

使用者的瀏覽器顯示從伺服器渲染並返回的頁面,一瞬間出現白屏,閃爍片刻,然後應用程式開始執行,看起來一切正常。出現閃爍的原因,在於 Angular 不知道如何重用它在伺服器上成功渲染的內容。在客戶端環境中,它從根元素中 strip 所有 HTML 並重新開始繪製。

閃爍問題可以抽象成如下步驟:

關於正在發生的事情的一個非常簡化的解釋:

(1) 使用者訪問應用程式(或重新整理)

(2) 伺服器在伺服器中構建html

(3) 它被髮送到使用者的瀏覽器端

(4) Angular 重新建立 應用程式(就好像它是一個常規的非 Angular Universal 程式)

(5) 當上述四個步驟發生時,使用者會看到一個 blink 即閃爍的螢幕。

程式碼如下:

// You can see this by adding:
// You should see a console log in the server
// `Running on the server with appId=my-app-id`
// and then you'll see in the browser console something like
// `Running on the browser with appId=my-app-id`
export class AppModule {
  constructor(
    @Inject(PLATFORM_ID) private platformId: Object,
    @Inject(APP_ID) private appId: string) {
    const platform = isPlatformBrowser(this.platformId) ?
      'in the browser' : 'on the server';
    console.log(`Running ${platform} with appId=${this.appId}`);
  }
}

無法透過 API 的方式終止渲染

什麼時候需要人為干預的方式終止一個伺服器端渲染?

始終明確一點,渲染應用程式的時間點發生在應用程式 applicationRef.isStable 返回 true 時,參考下列程式碼:

https://github.com/angular/an...

function _render<T>(
    platform: PlatformRef, moduleRefPromise: Promise<NgModuleRef<T>>): Promise<string> {
  return moduleRefPromise.then((moduleRef) => {
    const transitionId = moduleRef.injector.get(ɵTRANSITION_ID, null);
    if (!transitionId) {
      throw new Error(
          `renderModule[Factory]() requires the use of BrowserModule.withServerTransition() to ensure
the server-rendered app can be properly bootstrapped into a client app.`);
    }
    const applicationRef: ApplicationRef = moduleRef.injector.get(ApplicationRef);
    return applicationRef.isStable.pipe((first((isStable: boolean) => isStable)))
        .toPromise()
        .then(() => {
          const platformState = platform.injector.get(PlatformState);
         ...

相關文章