如何提高 Angular 應用程序的性能

已發表: 2020-04-10

在談論最偉大的前端框架時,不可能不提到 Angular。 但是,它需要程序員付出很多努力來學習和明智地使用它。 不幸的是,沒有 Angular 經驗的開發人員可能會以低效的方式使用它的某些功能。

作為前端開發人員,您始終需要處理的許多事情之一就是應用程序的性能。 我過去的大部分項目都集中在不斷擴展和開發的大型企業應用程序上。 前端框架在這裡將非常有用,但正確合理地使用它們很重要。

我準備了一份最流行的性能提升策略和技巧的快速列表,它們可以幫助您立即提高 Angular 應用程序的性能。 請記住,這裡的所有提示都適用於版本 8 中的 Angular。

ChangeDetectionStrategy 和 ChangeDetectorRef

變更檢測 (CD)是 Angular 用於檢測數據變更並自動對其做出反應的機制。 我們可以列出標準應用程序狀態更改的基本類型:

  • 活動
  • HTTP 請求
  • 計時器

這些是異步交互。 問題是:Angular 怎麼知道發生了一些交互(例如點擊、間隔、http 請求)並且需要更新應用程序狀態?

答案是ngZone ,它基本上是一個旨在跟踪異步交互的複雜系統。 如果所有操作都由 ngZone 註冊,Angular 就知道何時對某些更改做出反應。 但它不知道究竟發生了什麼變化,並啟動了變更檢測機制,該機制以一階深度檢查所有組件。

Angular 應用程序中的每個組件都有自己的變更檢測器,它定義了在啟動變更檢測時該組件應該如何操作——例如,如果需要重新渲染組件的 DOM(這是一項相當昂貴的操作)。 當 Angular 啟動變更檢測時,每個組件都會被檢查,並且默認情況下它的視圖(DOM)可能會被重新渲染。

我們可以通過使用 ChangeDetectionStrategy.OnPush 來避免這種情況:

 @零件({
  選擇器:'foobar',
  templateUrl: './foobar.component.html',
  styleUrls: ['./foobar.component.scss'],
  changeDetection:ChangeDetectionStrategy.OnPush
})

正如您在上面的示例代碼中看到的,我們必須向組件的裝飾器添加一個附加參數。 但是這種新的變化檢測策略是如何真正起作用的呢?

該策略告訴 Angular 一個特定的組件只依賴於它的@Inputs()。 此外,所有組件@Inputs() 將像一個不可變對像一樣工作(例如,當我們只更改對象的@Input() 中的屬性,而不更改引用時,不會檢查該組件)。 這意味著將省略許多不必要的檢查,並且應該會提高我們的應用程序性能。

只有在以下情況下才會檢查具有 ChangeDetectionStrategy.OnPush 的組件:

  • @Input() 引用將改變
  • 將在組件的模板或其子模板中觸發事件
  • 組件中的 Observable 會觸發一個事件
  • CD將使用 ChangeDetectorRef 服務手動運行
  • 在視圖上使用異步管道(異步管道標記要檢查更改的組件 - 當源流將發出新值時,將檢查該組件)

如果上述情況均未發生,則在特定組件內使用 ChangeDetectionStrategy.OnPush 會導致 CD 啟動後不檢查該組件和所有嵌套組件。

幸運的是,我們仍然可以使用 ChangeDetectorRef 服務完全控制對數據更改的反應。 我們必須記住,在我們的超時、請求、訂閱回調中使用 ChangeDetectionStrategy.OnPush,如果我們真的需要,我們需要手動觸發 CD:

 計數器 = 0;

構造函數(私有 changeDetectorRef:ChangeDetectorRef){}

ngOnInit() {
  設置超時(()=> {
    this.counter += 1000;
    this.changeDetectorRef.detectChanges();
  }, 1000);
}

正如我們在上面看到的,通過在超時函數中調用 this.changeDetectorRef.detectChanges(),我們可以手動強制CD 。 如果計數器以任何方式在模板內部使用,它的值將被刷新。

本節的最後一個技巧是關於永久禁用特定組件的CD 。 如果我們有一個靜態組件並且我們確定它的狀態不應該改變,我們可以永久禁用CD

 this.changeDetectorRef.detach()

這段代碼應該在 ngAfterViewInit() 或 ngAfterViewChecked() 生命週期方法中執行,以確保我們的視圖在禁用數據刷新之前正確呈現。 在CD期間將不再檢查此組件,除非我們手動觸發 detectChanges()。

模板中的函數調用和getter

每次更改檢測器運行時,在模板中使用函數調用都會執行此函數。 同樣的情況也發生在getter上。 如果可能的話,我們應該盡量避免這種情況。 在大多數情況下,我們不需要在每次CD運行期間執行組件模板中的任何函數。 取而代之的是,我們可以使用純管道。

純水管

純管道是一種輸出僅依賴於輸入的管道,沒有副作用。 幸運的是,Angular 中的所有管道默認都是純的。

 @管道({
    名稱:'大寫',
    純:真
})

但是我們為什麼要避免使用帶有 pure: false 的管道呢? 答案又是變更檢測。 在每次 CD 運行中都會執行非純管道,這在大多數情況下是不必要的,並且會降低我們應用程序的性能。 這是我們可以更改為純管道的函數示例:

 轉換(值:字符串,限制 = 60,省略號 = '...'){
  if (!value || value.length <= limit) {
    返回值;
  }
  const numberOfVisibleCharacters = value.substr(0, limit).lastIndexOf(' ');
  返回`${value.substr(0, numberOfVisibleCharacters)}${ellipsis}`;
}

讓我們看看視圖:

 <p class="description">截斷(文本,30)</p>

上面的代碼代表純函數——沒有副作用,輸出只依賴於輸入。 在這種情況下,我們可以簡單地用純管道替換這個函數:

 @管道({
  名稱:'截斷',
  純:真
})
導出類 TruncatePipe 實現 PipeTransform {
  轉換(值:字符串,限制 = 60,省略號 = '...'){
    ...
  }
}

最後,在這個視圖中,我們得到了代碼,該代碼僅在文本更改時執行,獨立於Change Detection

 <p class="description">{{ 文本 | 截斷:30 }}</p>

延遲加載和預加載模塊

當您的應用程序有多個頁面時,您絕對應該考慮為項目的每個邏輯部分創建模塊,尤其是延遲加載模塊。 讓我們考慮一下簡單的 Angular 路由器代碼:

 常量路線:路線= [
  {
    小路: '',
    組件:HomeComponent
  },
  {
    路徑:'foo',
    loadChildren: ()=> import("./foo/foo.module").then(m => m.FooModule)
  },
  {
    路徑:'酒吧',
    loadChildren: ()=> import("./bar/bar.module").then(m => m.BarModule)
  }
]
@NgModule({
  出口:[路由器模塊],
  進口:[RouterModule.forRoot(routes)]
})
類 AppRoutingModule {}

在上面的示例中,我們可以看到只有當用戶嘗試輸入特定路線(foo 或 bar)時,才會加載 fooModule 及其所有資產。 Angular 也會為這個模塊生成一個單獨的延遲加載會減少初始加載

我們可以做一些進一步的優化。 假設我們想讓我們的應用程序在後台加載模塊。 對於這種情況,我們可以使用 preloadingStrategy。 Angular 默認有兩種類型的 preloadingStrategy:

  • 無預加載
  • PreloadAllModules

在上面的代碼中,默認使用 NoPreloading 策略。 應用程序根據用戶請求開始加載特定模塊(當用戶想要查看特定路線時)。 我們可以通過向路由器添加一些額外的配置來改變這一點。

 @NgModule({
  出口:[路由器模塊],
  進口:[RouterModule.forRoot(路線,{
       preloadingStrategy: PreloadAllModules
  }]
})
類 AppRoutingModule {}

此配置會導致當前路由盡快顯示,之後應用程序將嘗試在後台加載其他模塊。 聰明,不是嗎? 但這還不是全部。 如果這個解決方案不符合我們的需求,我們可以簡單地編寫我們自己的自定義策略

假設我們只想預加載選定的模塊,例如 BarModule。 我們通過為數據字段添加一個額外的字段來表明這一點。

 常量路線:路線= [
  {
    小路: '',
    組件:HomeComponent
    數據:{預加載:假}
  },
  {
    路徑:'foo',
    loadChildren: ()=> import("./foo/foo.module").then(m => m.FooModule),
    數據:{預加載:假}
  },
  {
    路徑:'酒吧',
    loadChildren: ()=> import("./bar/bar.module").then(m => m.BarModule),
    數據:{預加載:真}
  }
]

然後我們必須編寫我們的自定義預加載函數:

 @Injectable()
導出類 CustomPreloadingStrategy 實現 PreloadingStrategy {
  preload(route: Route, load: () => Observable<any>): Observable<any> {
    返回 route.data && route.data.preload ? 加載():(空);
  }
}

並將其設置為 preloadingStrategy:

 @NgModule({
  出口:[路由器模塊],
  進口:[RouterModule.forRoot(路線,{
       preloadingStrategy: CustomPreloadingStrategy
  }]
})
類 AppRoutingModule {}

目前只預加載帶有參數 { data: { preload: true } } 的路由。 其餘路線將像設置了 NoPreloading 一樣。

自定義的 preloadingStrategy 是@Injectable(),所以這意味著如果我們需要的話,我們可以在裡面注入一些服務,並以任何其他方式自定義我們的 preloadingStrategy。
使用瀏覽器的開發人員工具,我們可以通過使用和不使用 preloadingStrategy 的相同初始加載時間來研究性能提升。 我們還可以查看網絡選項卡,以查看其他路由的塊正在後台加載,而用戶能夠毫無延遲地看到當前頁面。

追踪功能

我們可以假設大多數 Angular 應用程序使用 *ngFor 來迭代模板中列出的項目。 如果迭代列表也是可編輯的,那麼 trackBy 絕對是必須的。

 <ul>
  <tr *ngFor="let product of products; trackBy: trackByProductId">
    <td>{{ product.title }}</td>
  </tr>
</ul>

trackByProductId(索引:編號,產品:產品){
  返回product.id;
}

通過使用 trackBy 函數,Angular 能夠跟踪集合的哪些元素已更改(通過給定的標識符)並僅重新渲染這些特定元素。 當我們省略 trackBy 時,整個列表將被重新加載,這在 DOM 上可能是一個非常耗費資源的操作。

提前 (AOT) 編譯

關於 Angular 文檔:

(...) Angular 提供的組件和模板不能被瀏覽器直接理解,Angular 應用程序需要一個編譯過程才能在瀏覽器中運行

Angular 提供了兩種編譯方式:

  • 即時(JIT) – 在運行時在瀏覽器中編譯應用程序
  • Ahead-of-Time (AOT) – 在構建時編譯應用程序

對於開髮用途, JIT編譯應滿足開發人員的需求。 儘管如此,對於生產構建,我們絕對應該使用AOT 。 我們需要確保 angular.json 文件中的aot標誌設置為 true。 這種解決方案最重要的好處包括更快的渲染、更少的異步請求、更小的框架下載大小和更高的安全性。

概括

在項目的開發和維護部分,您需要牢記應用程序的性能。 但是,自行尋找可能的解決方案可能既費時又費力。 在開發過程中檢查這些常見的錯誤並牢記它們不僅可以幫助您立即提高 Angular 應用程序的性能,還可以幫助您避免未來的失誤。

發布產品圖標

使用您的 Angular 項目信任 Miquido

聯繫我們

想用 Miquido 開發應用程序?

考慮使用 Angular 應用程序為您的業務帶來助力? 與我們聯繫並選擇我們的 Angular 應用程序開發服務。