MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

优化Angular模块的性能表现

2022-06-012.9k 阅读

理解 Angular 模块

Angular 模块概述

在深入探讨性能优化之前,我们首先要对 Angular 模块有清晰的认识。Angular 模块(NgModule)是一种组织 Angular 应用的方式,它将相关的组件、指令、管道和服务组合在一起。一个典型的 Angular 应用由多个模块构成,其中根模块通常命名为 AppModule,它是应用启动的入口点。

Angular 模块通过 @NgModule 装饰器来定义,这个装饰器接收一个元数据对象,该对象包含了模块的各种配置信息。例如:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform - browser';
import { AppComponent } from './app.component';

@NgModule({
  imports: [BrowserModule],
  declarations: [AppComponent],
  bootstrap: [AppComponent]
})
export class AppModule {}

在上述代码中,imports 数组指定了该模块所依赖的其他模块,这里引入了 BrowserModule,它是在浏览器环境中运行 Angular 应用所必需的模块。declarations 数组列出了属于该模块的组件、指令和管道,这里只有 AppComponentbootstrap 数组指定了应用的根组件,即 AppComponent

模块的懒加载

懒加载是 Angular 模块的一个重要特性,它可以显著提升应用的性能。懒加载允许我们在需要的时候才加载模块及其相关的代码,而不是在应用启动时一次性加载所有模块。

实现模块懒加载的关键在于使用 loadChildren 语法。例如,假设我们有一个 UserModule,我们可以这样配置路由来实现懒加载:

const routes: Routes = [
  {
    path: 'users',
    loadChildren: () => import('./user/user.module').then(m => m.UserModule)
  }
];

在上述代码中,当用户访问 users 路径时,UserModule 才会被加载。这种延迟加载的方式可以减少初始加载时间,特别是对于大型应用,将应用划分为多个可懒加载的模块可以提高用户体验。

优化模块加载顺序

优先加载关键模块

在 Angular 应用中,并非所有模块的重要性都是相同的。有些模块包含了应用核心功能所需的组件、服务等,这些模块应该优先加载。比如,包含导航栏、用户认证逻辑的模块通常是关键模块。

假设我们有一个 CoreModule 包含了应用的核心服务和组件,如用户认证服务 AuthService 和全局导航组件 NavigationComponent。我们应该确保这个模块在应用启动时尽快加载。

在根模块 AppModule 中,我们可以这样配置:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform - browser';
import { CoreModule } from './core/core.module';
import { AppComponent } from './app.component';

@NgModule({
  imports: [BrowserModule, CoreModule],
  declarations: [AppComponent],
  bootstrap: [AppComponent]
})
export class AppModule {}

通过将 CoreModule 直接引入到 AppModuleimports 数组中,我们保证了它在应用启动时就被加载。

分析模块依赖关系

了解模块之间的依赖关系对于优化加载顺序至关重要。一个模块可能依赖于多个其他模块,而这些依赖模块又可能有自己的依赖,形成一个依赖树。

我们可以使用工具来分析模块的依赖关系,例如 @angular - cli 提供的一些命令行工具。通过分析依赖关系,我们可以确定哪些模块可以并行加载,哪些模块必须按顺序加载。

假设我们有 ModuleA 依赖于 ModuleBModuleC,而 ModuleB 又依赖于 ModuleD。我们可以绘制如下的依赖树:

ModuleA
|-- ModuleB
|   |-- ModuleD
|-- ModuleC

从这个依赖树中,我们可以看出 ModuleCModuleD 可以并行加载,因为它们之间没有直接的依赖关系,这样可以提高加载效率。

减少模块体积

移除未使用的代码

在开发过程中,随着项目的不断演进,模块中可能会积累一些未使用的代码,如未使用的组件、指令、管道或服务。这些未使用的代码不仅增加了模块的体积,还会影响加载和运行性能。

我们可以借助工具如 tree - shaking 来自动移除未使用的代码。在 Angular 项目中,当我们使用现代的构建工具(如 webpack,它是 @angular - cli 默认使用的构建工具)时,tree - shaking 功能会自动生效,前提是我们的代码采用了 ES6 模块语法进行导入和导出。

例如,假设我们有一个 SharedModule,其中有一个未使用的组件 UnusedComponent

import { NgModule } from '@angular/core';
import { UnusedComponent } from './unused.component';
import { SharedService } from './shared.service';

@NgModule({
  declarations: [UnusedComponent],
  providers: [SharedService],
  exports: []
})
export class SharedModule {}

如果我们在整个应用中都没有使用 UnusedComponent,当进行构建时,tree - shaking 会将与 UnusedComponent 相关的代码从最终的 bundle 文件中移除。

优化第三方库的引入

在 Angular 项目中,我们经常会引入第三方库来实现特定的功能,如图表绘制库、表单验证库等。然而,一些第三方库可能体积较大,并且我们可能只需要其中的部分功能。

lodash 库为例,它是一个功能丰富的 JavaScript 实用工具库,但整个库体积较大。如果我们只需要使用其中的 debounce 函数,我们可以通过以下方式优化引入:

import { debounce } from 'lodash';

// 使用 debounce 函数
const debouncedFunction = debounce(() => {
  console.log('Debounced function called');
}, 300);

而不是像这样引入整个库:

import * as _ from 'lodash';

// 使用 debounce 函数
const debouncedFunction = _.debounce(() => {
  console.log('Debounced function called');
}, 300);

通过这种方式,我们只引入了所需的功能,减少了模块体积。

优化模块内的组件和服务

组件的变更检测策略

Angular 中的组件有两种主要的变更检测策略:DefaultOnPush。默认情况下,组件使用 Default 策略,这意味着当任何数据发生变化时,Angular 会检查该组件及其子组件树中的所有组件。

然而,对于一些组件,特别是那些输入数据很少变化或者纯展示型的组件,我们可以将变更检测策略设置为 OnPush。当组件的变更检测策略为 OnPush 时,Angular 只会在以下情况下检查该组件:

  1. 组件的输入属性发生引用变化。
  2. 组件接收到事件(如点击事件)。
  3. 该组件或其祖先组件的变更检测策略为 Default 且发生了变更检测。

假设我们有一个 UserCardComponent,它接收一个 user 对象作为输入属性来展示用户信息,并且这个 user 对象在大多数情况下不会频繁变化。我们可以这样设置变更检测策略:

import { Component, Input, ChangeDetectionStrategy } from '@angular/core';

@Component({
  selector: 'app - user - card',
  templateUrl: './user - card.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserCardComponent {
  @Input() user: { name: string; age: number };
}

通过设置 ChangeDetectionStrategy.OnPush,我们可以减少不必要的变更检测,从而提升性能。

服务的单例模式优化

在 Angular 中,服务默认是单例的,这意味着在整个应用中,同一个服务只会有一个实例。然而,有时候我们可能会在模块之间错误地配置服务,导致出现多个实例,这会浪费内存并可能导致数据不一致等问题。

为了确保服务的单例性,我们应该在根模块中提供服务。例如,假设我们有一个 UserService,我们应该在 AppModule 中这样配置:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform - browser';
import { UserService } from './user.service';
import { AppComponent } from './app.component';

@NgModule({
  imports: [BrowserModule],
  declarations: [AppComponent],
  providers: [UserService],
  bootstrap: [AppComponent]
})
export class AppModule {}

这样,在整个应用中,UserService 只会有一个实例。如果我们在其他模块中也提供了 UserService,可能会导致出现多个实例,所以要避免这种情况。

利用模块的预加载

预加载策略的原理

Angular 提供了预加载策略,允许我们在应用启动后,在空闲时间预先加载一些模块,这样当用户真正需要访问这些模块对应的功能时,加载速度会更快。预加载策略的核心思想是在应用启动时,根据一定的规则(如网络状况、用户行为预测等),提前将一些模块的代码下载到本地。

Angular 提供了两种内置的预加载策略:PreloadAllModulesNoPreloadingPreloadAllModules 会在应用启动后,尽快预加载所有可懒加载的模块。NoPreloading 则不会进行任何预加载。

自定义预加载策略

除了使用内置的预加载策略,我们还可以自定义预加载策略。假设我们有一些模块,我们希望根据模块的优先级来进行预加载。我们可以创建一个自定义的预加载策略类。

首先,定义一个接口来表示模块的优先级:

export interface ModulePriority {
  path: string;
  priority: number;
}

然后,创建自定义预加载策略类:

import { Injectable } from '@angular/core';
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of } from 'rxjs';

@Injectable()
export class PriorityPreloadingStrategy implements PreloadingStrategy {
  constructor(private priorities: ModulePriority[]) {}

  preload(route: Route, load: () => Observable<any>): Observable<any> {
    const priority = this.priorities.find(p => p.path === route.path)?.priority;
    if (priority) {
      return load();
    }
    return of(null);
  }
}

在路由配置中使用这个自定义预加载策略:

const routes: Routes = [
  {
    path: 'high - priority - module',
    loadChildren: () => import('./high - priority - module/high - priority - module.module').then(m => m.HighPriorityModule)
  },
  {
    path: 'low - priority - module',
    loadChildren: () => import('./low - priority - module/low - priority - module.module').then(m => m.LowPriorityModule)
  }
];

const modulePriorities: ModulePriority[] = [
  { path: 'high - priority - module', priority: 1 },
  { path: 'low - priority - module', priority: 2 }
];

@NgModule({
  imports: [RouterModule.forRoot(routes, {
    preloadingStrategy: PriorityPreloadingStrategy
  })],
  providers: [
    {
      provide: PriorityPreloadingStrategy,
      useValue: new PriorityPreloadingStrategy(modulePriorities)
    }
  ],
  exports: [RouterModule]
})
export class AppRoutingModule {}

通过这种方式,我们可以根据模块的优先级来进行预加载,提高应用的性能。

模块的缓存机制

模块缓存的概念

在 Angular 应用中,模块缓存是指将已经加载过的模块进行缓存,当再次需要加载相同模块时,直接从缓存中获取,而不需要重新加载。这可以显著提高模块的加载速度,特别是在应用中频繁访问某些模块的情况下。

Angular 本身在一定程度上支持模块缓存,例如,对于懒加载的模块,一旦加载过,再次访问相同路径时,Angular 会尝试从缓存中获取模块。

实现自定义模块缓存

虽然 Angular 有默认的模块缓存机制,但在某些情况下,我们可能需要更精细的控制,例如根据特定条件来决定是否使用缓存。我们可以通过自定义服务来实现一个简单的模块缓存机制。

首先,创建一个 ModuleCacheService

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';

@Injectable()
export class ModuleCacheService {
  private cache: { [key: string]: Observable<any> } = {};

  getModule<T>(key: string, load: () => Observable<T>): Observable<T> {
    if (this.cache[key]) {
      return this.cache[key] as Observable<T>;
    }
    const module$ = load();
    this.cache[key] = module$;
    return module$;
  }
}

然后,在路由配置中使用这个服务来加载模块:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ModuleCacheService } from './module - cache.service';

const routes: Routes = [
  {
    path:'my - module',
    loadChildren: (cacheService: ModuleCacheService) => cacheService.getModule('my - module', () => import('./my - module/my - module.module').then(m => m.MyModule))
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes, {
    useHash: true,
    preloadingStrategy: PreloadAllModules,
    paramsInheritanceStrategy: 'always',
    onSameUrlNavigation:'reload',
    relativeLinkResolution: 'legacy',
    scrollPositionRestoration: 'enabled',
    anchorScrolling: 'enabled',
    scrollOffset: [0, 64]
  })],
  providers: [ModuleCacheService],
  exports: [RouterModule]
})
export class AppRoutingModule {}

通过这种自定义的模块缓存服务,我们可以更灵活地控制模块的缓存,提高应用的性能。

优化模块与服务器的交互

减少不必要的 API 请求

在模块中,我们经常会通过 API 请求从服务器获取数据。然而,一些不必要的 API 请求会增加服务器的负载和网络延迟,从而影响应用的性能。

我们可以通过缓存 API 响应数据来减少不必要的请求。例如,假设我们有一个 UserModule,其中的 UserService 需要从服务器获取用户列表。我们可以在 UserService 中添加缓存功能:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable()
export class UserService {
  private userListCache: any[] | null = null;

  constructor(private http: HttpClient) {}

  getUserList(): Observable<any[]> {
    if (this.userListCache) {
      return Observable.of(this.userListCache);
    }
    return this.http.get<any[]>('/api/users').pipe(
      tap((users) => {
        this.userListCache = users;
      })
    );
  }
}

在上述代码中,当第一次调用 getUserList 方法时,会向服务器发送请求获取用户列表,并将响应数据缓存起来。后续调用时,如果缓存中有数据,则直接返回缓存数据,避免了重复的 API 请求。

优化请求参数和响应数据

在与服务器交互时,优化请求参数和响应数据也可以提高性能。我们应该确保请求参数尽可能精简,只包含服务器处理请求所必需的信息。同样,服务器返回的响应数据也应该只包含应用所需的字段。

例如,假设我们有一个获取用户详细信息的 API,请求 URL 为 /api/users/:id。如果我们只需要用户的姓名和邮箱,我们可以在请求时向服务器指定所需的字段:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable()
export class UserService {
  constructor(private http: HttpClient) {}

  getUserDetails(id: string): Observable<{ name: string; email: string }> {
    return this.http.get<{ name: string; email: string }>(`/api/users/${id}?fields=name,email`);
  }
}

这样,服务器只会返回用户的姓名和邮箱,减少了响应数据的体积,从而提高了数据传输速度和应用性能。

通过以上多个方面对 Angular 模块进行优化,我们可以显著提升 Angular 应用的性能,为用户提供更流畅的体验。无论是在模块加载顺序、模块体积、组件和服务优化,还是在与服务器交互等方面,每一个细节的优化都可能对整体性能产生重要影响。在实际开发中,我们需要根据应用的具体需求和特点,综合运用这些优化技巧,打造高性能的 Angular 应用。