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

Angular生产版本的性能测试与优化

2023-08-304.3k 阅读

性能测试工具介绍

在对 Angular 生产版本进行性能测试时,有几款工具是开发者们常用的,它们各有特点和适用场景。

Lighthouse

Lighthouse 是一款开源且集成在 Chrome DevTools 中的自动化工具,用于提高网络应用的质量。它从性能、可访问性、最佳实践和 SEO 等多个维度对网页进行评估,并给出详细的报告和优化建议。

在 Angular 项目中使用 Lighthouse 非常便捷,只需在 Chrome 浏览器中打开待测试的 Angular 应用,然后按 Ctrl + Shift + I(Windows / Linux)或 Command + Option + I(Mac)打开 DevTools,切换到 “Lighthouse” 标签页,点击 “Generate report” 按钮即可生成报告。例如,在一个简单的 Angular 待办事项应用中,运行 Lighthouse 后,报告可能指出某些图片没有设置适当的 alt 属性影响可访问性,或者某些 JavaScript 脚本阻塞了渲染,影响性能。

Lighthouse 报告中的性能得分基于多个指标,如首次内容绘制(First Contentful Paint, FCP)、最大内容绘制(Largest Contentful Paint, LCP)、累积布局偏移(Cumulative Layout Shift, CLS)等。FCP 测量从页面开始加载到页面任何部分在屏幕上呈现的时间,这对于用户感知应用的响应速度至关重要。如果 Angular 应用在启动时执行了过多的初始化逻辑,可能会导致 FCP 时间过长。

WebPageTest

WebPageTest 是另一个强大的性能测试工具,它允许在多个地理位置和不同网络条件下测试网页性能。与 Lighthouse 不同,WebPageTest 更侧重于模拟真实用户的网络环境,如 3G、4G 等不同网络速度。

使用 WebPageTest 时,只需在其官网(https://www.webpagetest.org/)输入 Angular 应用的 URL,选择测试地点(如美国、欧洲等不同地区节点)和网络条件(如 Mobile 3G、Cable 等),然后启动测试。例如,若要测试一个面向全球用户的 Angular 电商应用,选择不同地区节点测试可以发现不同地域的用户加载应用速度的差异。

WebPageTest 提供详细的瀑布图,展示每个资源(如 JavaScript 文件、CSS 文件、图片等)的加载时间和顺序。这有助于发现哪些资源加载缓慢,例如某个 Angular 应用依赖的第三方库文件在特定网络条件下加载时间过长,就可以从瀑布图中直观地看出。

ngx - perf

ngx - perf 是专门为 Angular 应用设计的性能测试库。它基于 Jasmine 测试框架,通过注入性能测试服务到 Angular 组件或服务中,来测量代码块的执行时间。

首先,通过 npm install ngx - perf 安装该库。在 Angular 组件中使用时,如下代码示例:

import { Component } from '@angular/core';
import { PerfService } from 'ngx - perf';

@Component({
  selector: 'app - my - component',
  templateUrl: './my - component.html'
})
export class MyComponent {
  constructor(private perfService: PerfService) {
    this.perfService.start('my - operation');
    // 模拟一些耗时操作
    for (let i = 0; i < 1000000; i++) {
      // 这里可以是实际的业务逻辑
    }
    this.perfService.stop('my - operation');
    const result = this.perfService.get('my - operation');
    console.log(`Operation took ${result.duration} ms`);
  }
}

上述代码中,通过 perfService.start 方法标记一个操作的开始,perfService.stop 方法标记操作结束,然后通过 get 方法获取该操作的执行时间。ngx - perf 对于测量 Angular 应用内部特定代码逻辑的性能非常有用,尤其是在优化组件的初始化或数据处理逻辑时。

性能测试指标详解

理解性能测试指标是优化 Angular 生产版本性能的关键,以下详细介绍几个重要指标。

加载时间相关指标

  1. 首次内容绘制(First Contentful Paint, FCP): FCP 标志着浏览器开始在屏幕上渲染任何部分页面的时间点,包括文本、图像、SVG 等。在 Angular 应用中,这可能受到启动时的初始化逻辑、依赖项加载等因素影响。例如,如果应用在启动时需要从服务器获取大量配置数据,这可能会延迟 FCP。为了缩短 FCP,开发者可以优化服务器端响应时间,或者在客户端使用缓存机制。

  2. 最大内容绘制(Largest Contentful Paint, LCP): LCP 测量的是页面最大元素加载并呈现到屏幕上的时间。对于 Angular 应用,如果页面中有大型列表、图片轮播等,这些元素的加载和渲染会影响 LCP。比如,一个包含大量图片的 Angular 图片展示应用,如果图片没有进行适当的优化(如压缩、懒加载等),就会导致 LCP 时间过长。优化 LCP 可以从图片优化、延迟加载非关键内容等方面入手。

  3. 首次有意义绘制(First Meaningful Paint, FMP): FMP 指的是页面呈现出对用户有意义内容的时间点。在 Angular 单页应用(SPA)中,这可能是应用的主视图渲染完成的时间。例如,对于一个 Angular 博客应用,FMP 可能是文章列表首次出现在屏幕上的时间。要提高 FMP 性能,开发者可以确保关键 CSS 和 JavaScript 文件的优先加载,避免阻塞渲染的操作。

用户交互相关指标

  1. 可交互时间(Time to Interactive, TTI): TTI 表示页面达到完全可交互状态所需的时间,即用户可以与页面上的所有元素进行正常交互(如点击按钮、输入文本等)的时间点。在 Angular 应用中,这可能受到应用初始化过程中组件的渲染、数据绑定等操作影响。如果在组件渲染过程中有大量复杂的数据计算或 DOM 操作,会延长 TTI。可以通过优化组件的生命周期钩子函数,避免在 ngOnInit 等钩子中执行过多耗时操作来缩短 TTI。

  2. 累积布局偏移(Cumulative Layout Shift, CLS): CLS 衡量的是页面加载过程中发生的所有意外布局偏移的总和。在 Angular 应用中,这通常与动态加载内容(如图片加载、广告插入等)有关。例如,当图片没有设置固定的宽高,在图片加载时可能会导致周围元素的布局发生偏移,从而增加 CLS。为了降低 CLS,开发者可以在 HTML 中为图片设置明确的宽高属性,或者使用 CSS 的 aspect - ratio 属性来保持元素的纵横比。

资源相关指标

  1. 总字节数(Total Byte Size): 这是指加载整个页面所需的所有资源(包括 HTML、CSS、JavaScript、图片等)的总大小。在 Angular 应用中,随着功能的增加,应用的代码包大小可能会不断增长。例如,引入过多的第三方库或者没有对代码进行有效的压缩和树摇,都会导致总字节数增加。通过代码拆分、使用 CDN 加载常用库等方式可以减小总字节数。

  2. 请求数量(Number of Requests): 每次浏览器请求一个资源(如 JavaScript 文件、CSS 文件、图片等)都会产生一个 HTTP 请求。过多的请求会增加页面加载时间,因为每个请求都有一定的开销(如建立连接、握手等)。在 Angular 应用中,如果没有对资源进行合并和优化,可能会导致请求数量过多。例如,将多个小的 CSS 文件合并为一个,或者使用雪碧图(sprite)来减少图片请求数量。

性能优化策略 - 代码层面

从代码层面优化 Angular 应用性能可以显著提升用户体验,以下是几个重要的优化方向。

组件优化

  1. 按需加载组件(Lazy Loading Components): Angular 提供了强大的路由懒加载功能,允许将应用的不同部分(模块和组件)按需加载,而不是在应用启动时全部加载。这对于大型 Angular 应用非常有效,可以显著减少初始加载时间。

首先,在 Angular CLI 生成的项目中,创建一个新的模块和组件,例如一个用户设置模块和设置组件:

ng generate module user - settings
ng generate component user - settings/settings

然后,在 app - routing.module.ts 文件中配置懒加载路由:

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

上述代码中,当用户访问 /user - settings 路径时,才会加载 UserSettingsModule 及其相关组件。这样,在应用启动时,用户设置模块的代码不会被加载,从而加快了初始加载速度。

  1. OnPush 变更检测策略: Angular 的变更检测机制默认会检查应用中所有组件树的变化,这在大型应用中可能会导致性能问题。通过将组件的变更检测策略设置为 OnPush,可以显著减少不必要的变更检测。

在组件类中,通过 @Component 装饰器设置变更检测策略:

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

@Component({
  selector: 'app - my - component',
  templateUrl: './my - component.html',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class MyComponent {
  // 组件逻辑
}

当设置为 OnPush 时,Angular 只会在以下几种情况下检查该组件的变化:

  • 输入属性(@Input())值发生引用变化。
  • 接收到事件(如点击按钮等用户事件)。
  • 该组件或其祖先组件手动调用 ChangeDetectorRef.detectChanges() 方法。

这种策略适用于那些输入属性很少变化,并且主要依赖于自身内部状态的组件,如展示性组件。

  1. 避免在组件生命周期钩子中执行过多操作: 组件的生命周期钩子函数(如 ngOnInitngOnChanges 等)是 Angular 应用中很重要的部分,但如果在这些钩子中执行过多耗时操作,会影响组件的渲染性能。

例如,在 ngOnInit 中避免进行复杂的数据计算或大量的 DOM 操作。如果需要获取数据,可以使用 Observable 并结合 async 管道来异步获取数据,而不是在 ngOnInit 中阻塞式地获取。

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

@Component({
  selector: 'app - data - component',
  templateUrl: './data - component.html'
})
export class DataComponent {
  data$: Observable<any>;

  constructor(private http: HttpClient) {
    this.data$ = this.http.get('/api/data');
  }
}

在模板中使用 async 管道:

<div *ngIf="data$ | async as data">
  <!-- 展示数据 -->
</div>

这样,数据获取是异步进行的,不会阻塞组件的渲染。

服务优化

  1. 单例服务(Singleton Services): Angular 中的服务默认是单例的,即在整个应用中只有一个实例。合理利用这一特性可以避免重复创建对象和执行初始化逻辑,从而提高性能。

例如,创建一个用于管理用户登录状态的服务:

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

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  private isLoggedIn = false;

  login() {
    this.isLoggedIn = true;
  }

  logout() {
    this.isLoggedIn = false;
  }

  isAuthenticated() {
    return this.isLoggedIn;
  }
}

在不同的组件中注入该服务,它们共享同一个 AuthService 实例,避免了重复管理登录状态的开销。

  1. 服务复用和依赖注入优化: 在 Angular 应用中,合理复用服务和优化依赖注入可以减少不必要的对象创建和内存消耗。例如,如果多个组件依赖于同一个数据服务,确保在注入时遵循依赖注入树的规则,避免重复创建服务实例。

假设一个 DataService 用于获取和处理应用的数据,在 app.module.ts 中提供该服务:

import { NgModule } from '@angular/core';
import { DataService } from './data.service';

@NgModule({
  providers: [DataService]
})
export class AppModule {}

这样,所有需要 DataService 的组件都会从应用模块的注入器中获取同一个实例,而不是每个组件自己创建一个新的实例。

数据绑定优化

  1. 单向数据绑定(One - way Data Binding): 在 Angular 中,双向数据绑定([(ngModel)])虽然方便,但在性能上不如单向数据绑定。双向数据绑定会在每次数据变化时触发变更检测,而单向数据绑定(如 [property] 用于从组件到视图,(event) 用于从视图到组件)更加可控,性能更好。

例如,在一个输入框中,使用单向数据绑定结合 (input) 事件来更新组件中的数据:

<input [value]="userInput" (input)="onInputChange($event.target.value)">

在组件类中:

import { Component } from '@angular/core';

@Component({
  selector: 'app - input - component',
  templateUrl: './input - component.html'
})
export class InputComponent {
  userInput = '';

  onInputChange(value: string) {
    this.userInput = value;
  }
}

这样,只有在用户输入时才会触发组件的逻辑更新,而不是像双向数据绑定那样每次数据变化都触发变更检测。

  1. 减少模板中的表达式计算: 在 Angular 模板中,如果存在大量复杂的表达式计算,会增加变更检测的开销。尽量将复杂的计算逻辑移到组件类中,并在模板中直接使用计算结果。

例如,避免在模板中进行多次数据格式化计算:

<!-- 不好的做法 -->
<p>{{ (data | date:'yyyy - MM - dd HH:mm:ss') }}</p>

可以在组件类中提前计算好:

import { Component } from '@angular/core';
import { formatDate } from '@angular/common';

@Component({
  selector: 'app - date - component',
  templateUrl: './date - component.html'
})
export class DateComponent {
  formattedDate: string;

  constructor() {
    const now = new Date();
    this.formattedDate = formatDate(now, 'yyyy - MM - dd HH:mm:ss', 'en - US');
  }
}

在模板中:

<p>{{ formattedDate }}</p>

这样,变更检测时只需要检查 formattedDate 的值是否变化,而不需要每次都进行日期格式化计算。

性能优化策略 - 构建和部署层面

除了代码层面的优化,构建和部署过程中的优化措施也对 Angular 生产版本的性能有重要影响。

构建优化

  1. 代码压缩和混淆(Minification and Obfuscation): 在 Angular 应用的构建过程中,启用代码压缩和混淆可以显著减小输出文件的大小。Angular CLI 默认在生产模式构建时(ng build --prod)会进行代码压缩。

代码压缩会移除代码中的空格、注释等冗余信息,并对变量名进行缩短,从而减小文件大小。例如,一个原本包含详细注释和较长变量名的 JavaScript 文件,经过压缩后,文件大小会大幅减小,这样在网络传输过程中可以更快地下载到客户端。

混淆则是对代码进行进一步处理,使代码难以被反编译和理解,增加代码的安全性。虽然混淆主要目的是保护代码,但在一定程度上也会减小文件大小。

  1. Tree - shaking: Tree - shaking 是一种优化技术,它可以消除未使用的代码。在 Angular 应用中,随着功能的增加,可能会引入一些未使用的模块、组件或服务。通过 Tree - shaking,构建工具可以分析代码的依赖关系,只保留应用实际使用的部分。

例如,假设在 app.module.ts 中引入了一个大型的 UI 组件库,但实际上只使用了其中的几个组件:

import { NgModule } from '@angular/core';
import { SomeLargeUILibraryModule } from'some - large - ui - library';
import { AppComponent } from './app.component';

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

如果该 UI 组件库支持 ES6 模块,并且构建工具(如 Webpack,Angular CLI 基于 Webpack)配置正确,Tree - shaking 会自动剔除未使用的组件代码,从而减小最终的打包文件大小。

  1. 代码拆分(Code Splitting): 代码拆分是将应用的代码分割成多个较小的块,按需加载。这与组件的懒加载类似,但更侧重于代码层面的优化。

Angular CLI 支持通过动态导入(import())进行代码拆分。例如,对于一个大型的 Angular 应用,可能有一些功能模块(如用户报告生成模块)不常用,可以将其代码拆分出来:

import { Component } from '@angular/core';

@Component({
  selector: 'app - main - component',
  templateUrl: './main - component.html'
})
export class MainComponent {
  loadReportModule() {
    import('./report - module').then(module => {
      // 可以在这里使用模块中的组件或服务
    });
  }
}

这样,在应用启动时,报告模块的代码不会被加载,只有当用户调用 loadReportModule 方法时才会加载,从而提高了应用的初始加载性能。

部署优化

  1. CDN(Content Delivery Network): 使用 CDN 可以将应用的静态资源(如 JavaScript 文件、CSS 文件、图片等)分发到全球各地的服务器节点,用户在访问应用时,会从距离最近的节点获取资源,从而加快加载速度。

对于 Angular 应用,常见的做法是将一些第三方库(如 Angular 核心库、RxJS 等)通过 CDN 加载。例如,在 index.html 文件中,可以引入 CDN 链接:

<head>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/12.0.0/angular.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/7.5.0/rxjs.umd.min.js"></script>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.0.0/css/bootstrap.min.css">
</head>

这样,用户在加载应用时,这些常用库会从 CDN 快速获取,而不是从应用服务器下载,减轻了应用服务器的负载,同时提高了用户的加载体验。

  1. 缓存策略(Caching Strategies): 合理设置缓存策略可以显著提高 Angular 应用的性能。对于静态资源(如 CSS、JavaScript 文件),可以设置较长的缓存时间,因为这些文件在应用的版本更新前通常不会变化。

在服务器端配置缓存头信息,例如在 Node.js 应用中使用 Express 框架:

const express = require('express');
const app = express();

app.use('/static', express.static('dist/my - angular - app', {
  maxAge: 31536000 // 设置缓存时间为一年(以秒为单位)
}));

上述代码中,将 Angular 应用构建后的静态文件放在 /static 路径下,并设置了一年的缓存时间。这样,用户在首次访问应用后,后续再次访问时,如果文件没有变化,浏览器会直接从本地缓存中加载,大大提高了加载速度。

  1. 服务器渲染(Server - Side Rendering, SSR): 服务器渲染是指在服务器端生成 HTML 页面,然后将其发送到客户端。这对于 Angular 应用来说,可以显著提高首屏加载速度和 SEO 性能。

使用 Angular Universal 可以实现服务器渲染。首先,通过 ng add @nguniversal/express - engine 命令为 Angular 项目添加服务器渲染支持。然后,在服务器端(如使用 Express 框架)配置渲染逻辑:

const express = require('express');
const { ngExpressEngine } = require('@nguniversal/express - engine');
const { AppServerModule } = require('./src/main.server');
const { provideModuleMap } = require('@nguniversal/module - map - ngfactory - loader');

const app = express();

app.engine('html', ngExpressEngine({
  bootstrap: AppServerModule,
  providers: [
    provideModuleMap(require('./dist/my - angular - app/server/main - server - ngfactory.json'))
  ]
}));

app.set('view engine', 'html');
app.set('views', 'dist/my - angular - app/browser');

app.get('*', (req, res) => {
  res.render('index', { req });
});

const port = process.env.PORT || 4000;
app.listen(port, () => {
  console.log(`Server listening on port ${port}`);
});

通过服务器渲染,用户在访问应用时,会首先收到一个已经渲染好的 HTML 页面,而不是等待客户端 JavaScript 加载和渲染,从而提高了用户体验,特别是在网络条件较差的情况下。

性能优化案例分析

下面通过一个实际的 Angular 应用案例来展示性能优化的过程和效果。

案例背景

该 Angular 应用是一个企业级项目管理工具,包含项目列表展示、任务管理、团队协作等功能。随着功能的不断增加,应用的性能逐渐下降,用户反馈加载速度慢,操作卡顿等问题。

性能测试结果分析

  1. 加载时间指标: 首次内容绘制(FCP)时间为 3.5 秒,最大内容绘制(LCP)时间为 5 秒,这主要是因为应用在启动时需要从服务器获取大量项目数据,并且初始化了多个复杂组件。
  2. 用户交互指标: 可交互时间(TTI)为 7 秒,累积布局偏移(CLS)为 0.2,TTI 较长是由于组件渲染过程中的大量数据计算和 DOM 操作,CLS 较高是因为图片加载时没有设置固定尺寸,导致布局频繁变动。
  3. 资源指标: 总字节数达到了 2.5MB,请求数量为 50 个,主要是因为引入了过多的第三方库,并且没有对代码进行有效的压缩和合并。

优化措施实施

  1. 代码层面优化
  • 组件优化:对项目列表组件和任务管理组件设置 OnPush 变更检测策略,避免不必要的变更检测。同时,将一些复杂的初始化逻辑从 ngOnInit 钩子中移到异步操作中。例如,将项目数据获取改为使用 Observableasync 管道。
  • 服务优化:将用户认证服务和项目数据服务设置为单例,并优化依赖注入,确保在整个应用中只有一个实例。
  • 数据绑定优化:将表单中的双向数据绑定改为单向数据绑定结合 (input) 事件,减少变更检测的频率。同时,将模板中的复杂表达式计算移到组件类中。
  1. 构建层面优化
  • 代码压缩和混淆:确保在生产模式构建时(ng build --prod)启用代码压缩和混淆,进一步减小文件大小。
  • Tree - shaking:检查第三方库的引入,确保未使用的代码被剔除。例如,移除了一些未使用的 UI 组件库的部分代码。
  • 代码拆分:对一些不常用的功能模块(如团队成员管理模块)进行代码拆分,按需加载。
  1. 部署层面优化
  • CDN:将 Angular 核心库、RxJS 等常用库通过 CDN 加载,减轻应用服务器的负载。
  • 缓存策略:在服务器端设置静态资源的缓存时间为一年,提高资源的加载速度。
  • 服务器渲染:引入 Angular Universal 实现服务器渲染,提高首屏加载速度。

优化效果对比

经过一系列优化后,再次进行性能测试:

  1. 加载时间指标: FCP 时间缩短到 1.5 秒,LCP 时间缩短到 2.5 秒,加载速度有了显著提升。
  2. 用户交互指标: TTI 时间缩短到 3 秒,CLS 降低到 0.05,用户操作更加流畅,布局更加稳定。
  3. 资源指标: 总字节数减小到 1.2MB,请求数量减少到 30 个,网络传输压力明显减轻。

通过这个案例可以看出,综合运用代码、构建和部署层面的性能优化策略,可以有效提升 Angular 生产版本的性能,为用户提供更好的体验。