性能文章>如何使用 Chrome DevTools 检测和修复内存泄漏>

如何使用 Chrome DevTools 检测和修复内存泄漏转载

2年前
485912

去年,我的团队给我分配了一项任务,即解决我们的一个 Angular 应用程序中的性能问题。那一刻,我很害怕。我觉得我受到了这项任务的惩罚。

 

目标应用程序的很大一部分是由我编写的,但我不知道该怎么做才能修复它。该应用程序一开始运行平稳,但在添加了新功能(例如 Angular 材料表的内联编辑)后,它的运行速度越来越慢,并且加载时间很长,最终用户对此完全不满意。

 

一开始,我很挣扎。性能太差了,即使是 Lighthouse 也无法开始运行。由于很长的First Contentful Paint (FCP)和Time To Interactive (TTI)导致 Lighthouse 出现错误,但几天后,我看到 Lighthouse Audit 和 Chrome DevTools 的结果有所改善,我的热情增加了性能分析。

 

 

在展示了我在性能调整过程中为实现这一结果所做的工作之后,我从团队和利益相关者那里得到了非常积极、令人鼓舞的反馈。

 

几周后,我的团队正在处理的第二个 Angular 应用程序开始遇到类似的问题,但症状不同。

 

最终用户并没有对加载时间感到沮丧,但是在长时间使用该应用程序后,他们意识到它变得更慢、迟钝并且似乎经常暂停。这一次,我不是害怕,而是好奇造成问题的原因。

 

经过一番调查,原来罪魁祸首是内存泄漏

 

说话便宜。让我们创建一些代码,看看我们如何生成一个遭受内存泄漏影响性能并促使用户讨厌它的 Web 应用程序。

项目设置

你知道该怎么做。启动您的终端,运行命令ng new,并提供apngular-memory-leaks创建应用程序的名称: 

ng new angular-memory-leaks 
cd angular-memory-leaks

ng new命令会提示您有关要包含在初始应用程序中的功能的信息。您可以通过按 Enter 或 Return 键接受默认值。

代码

好了,到了好东西的时间了。我们遵循接下来的步骤。

 

  • 安装Angular Material和Angular Flex 布局:
npm install @angular/material
npm install @angular/cdk
npm install hammerjs
npm install @angular/flex-layout

 

  • 通过将此行添加到以下内容来导入 Angular 主题src/style.scss
@import "~@angular/material/prebuilt-themes/indigo-pink.css";

 

  • 生成两个新组件,todo-list并且todo-dialog
ng generate component todo-list
ng generate component todo-dialog

 

  • 更新app.module.ts
@NgModule({
  declarations: [ AppComponent, TodoListComponent, TodoDialog ],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    ReactiveFormsModule,
    RouterModule.forRoot([{path: '', component: TodoListComponent}]),
    MatListModule,
    MatButtonModule,
    MatCardModule,
    MatDialogModule,
    MatFormFieldModule,
    MatTooltipModule,
    FlexModule,
    MatInputModule,
    MatSelectModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}
 app.module.ts

 

  • 将 的内容替换为app.component.html 以下内容:
<div class="container">
  <router-outlet></router-outlet>
</div>
app.component.html

 

  • 这就是todo-list.component.html它的样子:
<div class="todo-list">
  <div fxLayout="row wrap" fxLayoutGap="10px">

    <mat-card class="todo-card" *ngFor="let todo of todoList" fxFlex [class.mat-elevation-z8]>
      <mat-card-header>
        <div mat-card-avatar class="example-header-image"></div>
        <mat-card-title>{{todo.id}} - {{todo.name}}</mat-card-title>
        <mat-card-subtitle>Type: {{todo.type}}</mat-card-subtitle>
      </mat-card-header>
      <mat-card-content>
        <p>{{todo.description}}</p>
      </mat-card-content>
      <mat-card-actions>
        <div fxLayoutAlign="space-between">
          <div *ngIf="!todo.dependencies"></div>
          <div *ngIf="todo.dependencies" class="dependencies" fxLayout="row" fxLayoutGap="5px" fxLayoutAlign="end center">
            <i class="material-icons">all_inclusive</i>
            <div>{{todo.dependencies.name}}</div>
          </div>
          <div>
            <button mat-icon-button color="primary" (click)="updateTodo(todo)" matTooltip="Edit TODO">
              <i class="material-icons">edit</i>
            </button>
            <button mat-icon-button color="primary" (click)="deleteTodo(todo.id)" matTooltip="Delete TODO">
              <i class="material-icons">delete_outline</i>
            </button>
          </div>
        </div>
      </mat-card-actions>
    </mat-card>
  </div>
  <div fxLayout="row" fxLayoutAlign="end center">
    <button mat-icon-button color="primary" (click)="createTodo()" matTooltip="Add TODO">
      <i class="material-icons">add_circle</i>
    </button>
  </div>
</div>
 todo-list.component.html
  • todo-list.component.css:
.todo-list {
  margin: 10px;
}

.todo-card {
  min-width: 300px;
  max-width: 300px;
  margin-bottom: 15px;
}

.dependencies {
  color: rgba(0,0,0,.54);
}

todo-list.component.css
  • todo-list.component.ts:
export interface TODO {
  id:            number;
  name:          string;
  type:          string; // = 'Coding' | 'Reading' | 'Writing';
  description?:  string;
  dependencies?: TODO;
}

@Component({
  selector: 'app-todo-list',
  templateUrl: './todo-list.component.html',
  styleUrls: ['./todo-list.component.css']
})
export class TodoListComponent implements OnInit {

  todoList: TODO[];

  constructor(public dialog: MatDialog, public todoService: TodoService) {}

  ngOnInit() {
    this.todoService.getTodos().subscribe(todos => this.todoList = todos);
  }

  createTodo(): void {
    const dialogRef = this.dialog.open(TodoDialog, {
      width: '300px',
      data: { mode: 'create' }
    });
    dialogRef.afterClosed().subscribe((result: TODO) => {});
  }

  updateTodo(todo: TODO) {
    const dialogRef = this.dialog.open(TodoDialog, {
      width: '300px',
      data: { todo: todo, mode: 'update' }
    });
    dialogRef.afterClosed().subscribe((result: TODO) => {});
  }

  deleteTodo(id: number) {
    this.todoService.deleteTodo(id);
  }
}

todo-list.component.ts
  • todo.dialog.html:
 
<h1 mat-dialog-title>Create new TODO</h1>

<div mat-dialog-content [formGroup]="todoForm">
  <mat-form-field>
    <mat-label>Name</mat-label>
    <input formControlName="name" matInput required/>
  </mat-form-field>

  <mat-form-field>
    <mat-label>Select a type</mat-label>
    <mat-select formControlName="type" required>
      <mat-option *ngFor="let typeTodo of types" [value]="typeTodo">{{typeTodo}}</mat-option>
    </mat-select>
    <!--mat-error *ngIf="type.hasError('required')">Please choose a type</mat-error-->
  </mat-form-field>

  <mat-form-field>
    <mat-label>Depends on</mat-label>
    <mat-select formControlName="dependencies">
      <mat-option *ngFor="let todo of todoList" [value]="todo">{{todo.name}}</mat-option>
    </mat-select>
  </mat-form-field>

  <mat-form-field>
    <mat-label>Description</mat-label>
    <textarea formControlName="description" matInput></textarea>
  </mat-form-field>
</div>

<div mat-dialog-actions>
  <button mat-button [mat-dialog-close]>Cancel</button>
  <button mat-button (click)="save()" cdkFocusInitial>Save</button>
</div>

todo.dialog.html

  • todo.dialog.ts:
 
export interface TodoDialogData {
  todo?: TODO;
  mode: string; // 'create' | 'update'
}

@Component({
  selector: 'todo.dialog',
  templateUrl: 'todo.dialog.html',
  styleUrls: ['./todo.dialog.css']
})
export class TodoDialog implements OnInit {

  todoForm:     FormGroup;
  name:         string;
  type:         string;
  dependencies: TODO;
  description:  string;
  types: string[]  = [];
  todoList: TODO[] = [];

  constructor(private formBuilder: FormBuilder,
              public todoService: TodoService,
              public dialogRef: MatDialogRef<TodoDialog>,
              @Inject(MAT_DIALOG_DATA) public data: TodoDialogData) {
  }

  ngOnInit() {
    this.todoService.getTodos().subscribe(todos => this.todoList = todos);
    this.todoService.getTypes().subscribe(types => this.types = types);

    this.todoForm = this.formBuilder.group({
      id:           [this.data.todo?.id],
      name:         [this.data.todo?.name        , [Validators.required]], // new FormControl(this.event.title),
      type:         [this.data.todo?.type        , [Validators.required]],
      dependencies: [this.data.todo?.dependencies, []],
      description:  [this.data.todo?.description , []]
    });
  }

  save(): void {
    this.todoService.updateTodoList(this.todoForm.value, this.data.mode);
    this.dialogRef.close(this.todoForm.value);
  }
}
todo.dialog.ts

  • 正如你所注意到的,todo-list.component.ts并且todo.dialog.ts 正在使用todo.service.ts,它提供了所有待办事项的类型和列表。现在,getTodos()方法getTypes()正在读取两个常量,但您可以根据自己的情况调整它们,以通过 REST 调用从后端获取真实数据:
@Injectable({
  providedIn: 'root'
})
export class TodoService {

  todoList: TODO[];
  types: string[];

  constructor() {}

  getTodos(): Observable<TODO[]> {
    this.todoList = TODOS;
    return of(this.todoList);
  }

  getTypes(): Observable<string[]> {
    this.types = TYPES;
    return of(this.types);
  }

  updateTodoList(todo: TODO, mode: string) {
    if (mode === 'create') {
      todo.id = this.todoList.length + 1;
      this.todoList.push(todo);
    }
    if (mode === 'update') {
      this.todoList[todo.id - 1] = todo;
    }
  }

  deleteTodo(id: number) {
    this.todoList.splice(id - 1, 1);

    this.todoList.forEach((todo, index) => {
      if (todo.dependencies?.id === id) {
        todo.dependencies = null;
      }
      todo.id = index + 1;
    });
  }
}
          
export const TYPES: string[] = ['Coding', 'Reading', 'Writing'];

export const TODOS: TODO[] = [{
    name: 'Read tutorial',
    id: 1,
    type: 'Reading',
    description: 'A great phone with one of the best cameras'
  }, {
    name: 'Write article',
    id: 2,
    type: 'Writing',
    description: 'Angular 9 app with memory leaks'
  }, {
    name: 'Implement app',
    id: 3,
    type: 'Coding',
    description: 'Angular 9 app with memory leaks'
}];
 

这是用浏览器运行ng serve和调用后的结果:localhost:4200

堆快照时间

 

让我们看看Chrome DevTools (F12)的一些统计数据。我们将拍摄两个堆快照,向我们展示在快照时间点内存是如何在应用程序的 JavaScript(对象、原语、字符串、函数、DOM 节点等)之间分布的。

  1. 重新加载页面 (F5) 后,在 DevTools 上打开内存面板。
  2. 启用堆快照复选框。
  3. 单击“拍摄快照”按钮。“快照 1”现已准备就绪。

堆快照

 

4. 使用您的网络应用程序:使用待办事项对话框创建八张新的待办事项卡片(单击加号“+”按钮)。

5. 然后单击“Take heap snapshot”图标进行第二个。第二个记录的快照将具有比第一个更大的大小:8.4 Mb 而不是 5.5 Mb。

6. 单击摘要,然后选择比较以查看差异。在“#New”列下,有第二个快照中新分配的对象(新数组、闭包、事件发射器、主题......)。在“# Deleted”列下,有已删除的对象。

 

新功能请求

我们可以开始添加一些有用的功能,例如使用 todo 对话框不仅可以创建一个 todo,而且可以同时创建多个 todo,并在 todo 对话框的“Depends on”下拉列表中添加一个条件:

  • “写作”类型的待办事项可能取决于三种待办事项类型:“写作”、“阅读”或“编码”。
  • “阅读”或“编码”类型的待办事项可能仅取决于“阅读”或“编码”。

对于这个实现,我们将使用 AngularFormArray 并订阅valueChanges该类型的字段。您必须更新todo.dialog.ts:如下:

export class TodoDialog implements OnInit {

  todosForm:        FormGroup;
  type:             string;
  types:            string[] = [];
  todoList:         TODO[]   = [];
  filteredTodoList: TODO[]   = [];
  mode:             string   = 'create'; // 'create' | 'update'

  constructor(private formBuilder: FormBuilder,
              public todoService: TodoService,
              public dialogRef: MatDialogRef<TodoDialog>,
              @Inject(MAT_DIALOG_DATA) public data: TodoDialogData) {
  }

  get todosFormArray(): FormArray {
   return this.todosForm.get('todos') as FormArray;
  }

  ngOnInit() {
    this.mode = this.data.mode;

    this.todoService.getTodos().subscribe(todos => {
      this.todoList         = todos;
      this.filteredTodoList = this.todoList;
    });
    this.todoService.getTypes().subscribe(types => this.types = types);

    const index     = 0;
    const formGroup = this.createTodoForm(this.data.todos[0], index);
    this.todosForm  = this.formBuilder.group({
      todos: new FormArray([formGroup])
    });
  }

  addTodo() {
    const index     = this.todosFormArray.controls.length;
    const formGroup = this.createTodoForm(null, index);
    this.todosFormArray.push(formGroup);
  }

  createTodoForm(todo: TODO, index: number): FormGroup {
    const formGroup = this.formBuilder.group({
      index:        [index],
      id:           [todo?.id],
      name:         [todo?.name        , [Validators.required]],
      type:         [todo?.type        , [Validators.required]],
      dependencies: [todo?.dependencies, []],
      description:  [todo?.description , []]
    });
    formGroup.valueChanges.subscribe(value => {
      console.log('form ' + index + ': value changed');
    });
    formGroup.get('type').valueChanges.subscribe(selectedType => {
      console.log('form ' + index + ': type changed to: ' + selectedType);
      this.filteredTodoList = this.filterTodoListPerType(selectedType);
    });
    return formGroup;
  }

  filterTodoListPerType(type: string): TODO[] {
    return this.todoList.filter(todo => {
      if (type === 'Writing') {
        return true;
      } else {
        return todo.type !== 'Writing';
      }
    });
  }

  deleteTodoForm(index: number) {
    this.todosFormArray.removeAt(index);
  }

  save(): void {
    this.dialogRef.close(this.todosFormArray.value);
  }
}
todo.dialog.ts

todo.dialog.html

<div mat-dialog-content>
  <div fxLayout="row" fxLayoutGap="10px">

  <form [formGroup]="todosForm">
    <div formArrayName="todos" *ngFor="let item of todosFormArray.controls; let i = index;">

      <div fxLayout="row" [formGroupName]="i" fxLayoutAlign="start center">
        <mat-form-field>
          <mat-label>Name</mat-label>
          <input formControlName="name" matInput required/>
        </mat-form-field>

        <mat-form-field>
          <mat-label>Select a type</mat-label>
          <mat-select formControlName="type" required>
            <mat-option *ngFor="let typeTodo of types" [value]="typeTodo">{{typeTodo}}</mat-option>
          </mat-select>
        </mat-form-field>

        <mat-form-field>
          <mat-label>Depends on</mat-label>
          <mat-select formControlName="dependencies">
            <mat-option *ngFor="let todo of filteredTodoList" [value]="todo">{{todo.name}}</mat-option>
          </mat-select>
        </mat-form-field>

        <mat-form-field>
          <mat-label>Description</mat-label>
          <textarea formControlName="description" matInput></textarea>
        </mat-form-field>

        <div *ngIf="mode === 'create' && todosFormArray.length > 1">
          <button mat-icon-button color="primary" (click)="deleteTodoForm(i)" matTooltip="Delete TODO">
            <i class="material-icons">delete</i>
          </button>
        </div>
      </div>

    </div>
  </form>
  </div>

  <div *ngIf="mode ==<div mat-dialog-content>
  <div fxLayout="row" fxLayoutWrap fxLayoutGap="10px">

  <form [formGroup]="todosForm">
    <div formArrayName="todos" *ngFor="let item of todosFormArray.controls; let i = index;">

      <div fxLayout="row" [formGroupName]="i" fxLayoutAlign="start center">
        <mat-form-field>
          <mat-label>Name</mat-label>
          <input formControlName="name" matInput required/>
        </mat-form-field>

        <mat-form-field>
          <mat-label>Select a type</mat-label>
          <mat-select formControlName="type" required>
            <mat-option *ngFor="let typeTodo of types" [value]="typeTodo">{{typeTodo}}</mat-option>
          </mat-select>
        </mat-form-field>

        <mat-form-field>
          <mat-label>Depends on</mat-label>
          <mat-select formControlName="dependencies">
            <mat-option *ngFor="let todo of filteredTodoList" [value]="todo">{{todo.name}}</mat-option>
          </mat-select>
        </mat-form-field>

        <mat-form-field>
          <mat-label>Description</mat-label>
          <textarea formControlName="description" matInput></textarea>
        </mat-form-field>

        <div *ngIf="mode === 'create' && todosFormArray.length > 1">
          <button mat-icon-button color="primary" (click)="deleteTodoForm(i)" matTooltip="Delete TODO">
            <i class="material-icons">delete</i>
          </button>
        </div>
      </div>

    </div>
  </form>
  </div>

  <div *ngIf="mode === 'create'" fxLayout="row" fxLayoutAlign="end center">
    <button mat-icon-button color="primary" (click)="addTodo()" matTooltip="Add TODO">
      <i class="material-icons">add_circle</i>
    </button>
  </div>
</div>

<div mat-dialog-actions>
  <button mat-button [mat-dialog-close]>Cancel</button>
  <button mat-button (click)="save()" [disabled]="todosForm.invalid">Save</button>
</div>= 'create'" fxLayout="row" fxLayoutAlign="end center">
    <button mat-icon-button color="primary" (click)="addTodo()" matTooltip="Add TODO">
      <i class="material-icons">add_circle</i>
    </button>
  </div>
</div>

<div mat-dialog-actions>
  <button mat-button [mat-dialog-close]>Cancel</button>
  <button mat-button (click)="save()" [disabled]="todosForm.invalid">Save</button>
</div>
todo.dialog.html

我们将在 and 中删除andcreateTodo()方法updateTodo()并使用openTodoDialog()method 代替:todo-list.component.tstodo-list.component.html

openTodoDialog(mode: string, todo?: TODO): void { // mode = 'create' | 'update'
    const dialogRef = this.dialog.open(TodoDialog, {
      width: '800px',
      data: { mode: mode, todos: todo ? [todo] : [] }
    });
    dialogRef.afterClosed().subscribe((result: TODO[]) => {
      if (result) {
        this.todoService.updateTodoList(result, mode);
      }
    });
todo-list.component.ts

我们必须调整updateTodoList()方法todo.service.ts

updateTodoList(todos: TODO[], mode: string) {
    // if the todo-dialog is opened with 'update' mode, it will contain only 1 TODO
    if (mode === 'update') {
      this.todoList[todos[0].id - 1] = todos[0];
    }

    if (mode === 'create') {
      todos.forEach(todo => {
        todo.id = this.todoList.length + 1;
        this.todoList.push(todo);
      });
    }
  }
todo.service.ts

新的待办事项对话框布局:

 

 

这很漂亮。现在是时候说出真相了。您需要重复前面的步骤 1 到 6 来比较当前状态的两个新堆快照(初始的一个和创建许多待办事项列表的场景之后的第二个)。结果将类似于以下内容:

 

如图所见,所需的堆大小在第二个快照中增加了 3 MB。创建了许多新的对象、侦听器、数组、DOM,但没有或很少有它们被删除。

绩效时间表记录

让我们用性能记录来翻译它。打开 DevTools 上的 Performance 面板,然后启用 Memory 复选框并进行记录。

 

 

单击开始按钮后,在停止记录之前,您需要使用应用程序:多次打开待办事项对话框,创建新的待办事项,向对话框中添加新表单,删除其中一些保存和不保存,并更新一些待办事项。停止记录,等到可以看到结果:

 

 

这里发生了什么?

 

Chrome 和 DevTools 为我们提供了发现影响页面性能的内存问题的可能性,包括内存泄漏、内存膨胀和频繁的垃圾收集。在上面的记录中,内存使用情况被细分为:

  • JS堆(Javascript需要的内存,蓝线)
  • 文件(红线)
  • DOM 节点(绿线)
  • 听众(黄线)
  • 显存

我们注意到 JS 堆的结尾比它开始时要高。在现实世界中,如果您看到这种增加的模式(JS 堆大小、节点大小、侦听器大小),则可能意味着内存泄漏。当应用程序无法摆脱未使用的资源并且用户意识到在某些时候应用程序变慢、迟缓并且可能会频繁暂停时,就会发生内存泄漏,这是潜在垃圾收集问题的征兆。

在性能时间线记录中,频繁上升和下降的 JS 堆或节点计数图意味着频繁的垃圾回收(垂直蓝线),这就是我们示例中的情况。

识别 JS 堆内存泄漏

  1. 打开开发工具
  2. 转到内存面板。
  3. 选择“时间轴上的分配工具”单选按钮。
  4. 按开始按钮(黑色圆圈)。
  5. 执行您怀疑导致内存泄漏的操作。
  6. 完成后按“停止录制”按钮(红色圆圈)。

 

 

每条蓝色垂直线都是为一些 JS 对象分配的内存。可以用鼠标选择一条线以查看有关它的更多详细信息。

我们有泄漏 - 我们如何修复它?

以下是一些可能导致 Angular 应用程序内存泄漏的情况:

  • 缺少取消订阅,这将保留内存中的组件
  • 缺少 DOM 事件侦听器的注销:例如滚动事件的侦听器、表单onChange事件的侦听器等。
  • 未使用时未关闭的WebSocket 连接
  • 分离的 DOM 树:当没有对它的全局引用时,可以对 DOM 节点进行垃圾收集。当一个节点从 DOM 树中移除时,它被称为分离,但一些 JavaScript 仍然引用它。这种情况可以通过比较两个堆快照然后向下滚动到 Constructor 列下以 Detached 为前缀的元素来识别。

当某个对象不再被全局对象可访问的对象引用时,垃圾收集器将可以访问该对象。相互引用但同时无法从根访问的对象将被垃圾收集。

我们的 todo 应用程序已经遭受了前面提到的两个导致内存泄漏的常见原因。每次需要订阅 observable 时,它​​都会生成一个 Subscription 对象,当组件被 Angular 运行时销毁时,应该以一种不会导致 JavaScript 运行时内存泄漏的方式处理该对象——这意味着调用unsubscribe(),通常在组件的ngOnDestroy方法内部。

在维护订阅formGroup.get('type').valueChanges和删除订阅后formGroup.valueChanges(因为不需要),我们在 和 中添加了缺少的取消订阅,todo.dialog.ts如下todo-list.component.ts所示:

import { Subject, Subscription } from 'rxjs';
import { takeUntil             } from 'rxjs/operators';

export class TodoDialog implements OnInit, OnDestroy {
  destroy$: Subject<boolean> = new Subject<boolean>();
  typeChangesUnsubscriptions: Subscription[] = [];
    
   ngOnInit() {
    this.todoService.getTodos().pipe(takeUntil(this.destroy$)).subscribe(todos => {
      // ...
    });
    this.todoService.getTypes().pipe(takeUntil(this.destroy$)).subscribe(types => this.types = types);
  }
    
  createTodoForm(todo: TODO, index: number): FormGroup {
    const formGroup = this.formBuilder.group({
      // ...
    });
    this.typeChangesUnsubscriptions[index] = formGroup.get('type').valueChanges.subscribe(selectedType => {
      this.filteredTodoList = this.filterTodoListPerType(selectedType);
    });
    return formGroup;
  }
    
  ngOnDestroy(): void {
    this.destroy$.next(true);
    this.destroy$.unsubscribe();
    this.typeChangesUnsubscriptions.forEach(value => value.unsubscribe());
  }
}
    
export class TodoListComponent implements OnInit, OnDestroy {
  destroy$: Subject<boolean> = new Subject<boolean>();
  
  ngOnInit() {
    this.todoService.getTodos().pipe(takeUntil(this.destroy$)).subscribe(todos => this.todoList = todos);
  }
    
  ngOnDestroy(): void {
    this.destroy$.next(true);
    this.destroy$.unsubscribe();
  }
}
取消订阅 observables

为了对更改传播和 DOM 事件发出更细粒度的控制,我们可以使用onlySelf: trueandemitEvent: false来防止表单字段的更改触发onChange其祖先的方法(整个表单或对话框中所有表单的列表)。

在移除监听器 to 之前FormGroup valueChanges,您可能已经注意到,每次更改 Type 下拉列表中的值后,浏览器控制台上都会显示两条日志消息:一条来自console.log('form value changed'),另一条来自,console.log('type changed')因为触发了字段事件监听器已传播到父 DOM 事件侦听器。

每当删除不需要的待办事项表单(带有删除图标)时,您必须记住取消订阅其侦听器:

deleteTodoForm(formIndex: number) {
    this.typeChangesUnsubscriptions[formIndex].unsubscribe();
    this.todosFormArray.removeAt(formIndex);
    this.todosFormArray.controls.forEach((formGroup, index) => {
      formGroup.get('index').setValue(index, {onlySelf: true, emitEvent: false});
    });
  }
formArray

去重复前面的场景,然后为它做一个性能时间线记录。走着瞧吧; 我会在这里等。注意到什么有趣的事情了吗?有很大的不同吗?

我不这么认为。这种改进不是灵丹妙药。这些内存泄漏还有更多的问题。所以,让我们继续我们的优化任务。

 

我在 todo 对话框中ChangeDetectionStrategy使用了该策略,而不是默认的 Angular :OnPush

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

@Component({
  selector: 'todo.dialog',
  templateUrl: 'todo.dialog.html',
  styleUrls: ['./todo.dialog.css'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TodoDialog implements OnInit, OnDestroy { /* ... */ }
todo.dialog.ts 中的 OnPush ChangeDetectionStrategy

我实现了一个定制的纯管道并在现场使用它。现在不再需要订阅 type's了。 selectBoxvalueChanges

请记住,不纯管道被频繁调用,就像每次击键或鼠标移动一样频繁,并且昂贵且长时间运行的管道可能会破坏用户体验。

<mat-form-field>
    <mat-label>Depends on</mat-label>
    <mat-select formControlName="dependencies">
        <mat-option *ngFor="let todo of (dependencies | filterPerType: item.get('type').value)" [value]="todo">
            {{todo.name}}
        </mat-option>
     </mat-select>
</mat-form-field>
带管道的选择框

不要忘记添加FilterPerTypePipe到声明部分app.module.ts

import { Pipe, PipeTransform } from '@angular/core';
import { TODO } from './todo.model';

@Pipe({
  name: 'filterPerType',
  pure: true
})
export class FilterPerTypePipe implements PipeTransform {

  transform(allTodos: TODO[], type: string): TODO[] {
    return allTodos.filter(todo => {
      if (type === 'Coding' || type === 'Reading') {
        return todo.type !== 'Writing';
      } else {
        return true;
      }
    });
  }
}
每个类型的过滤器.pipe.ts

todoService.getTodos()我在对话框中删除了对的订阅,因为todoList它已经在待办事项列表组件中可用并且可以添加到TodoDialogData. 我还将订阅移至todoService.getTypes()待办事项列表组件。不需要TodoService注射TodoDialog

@Component({
  selector: 'todo.dialog',
  templateUrl: 'todo.dialog.html',
  styleUrls: ['./todo.dialog.css'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TodoDialog implements OnInit {

  todosForm:        FormGroup;
  type:             string;
  types:            string[] = [];
  dependencies:     TODO[]   = [];
  mode:             string   = 'create'; // 'create' | 'update'

  constructor(private formBuilder: FormBuilder,
              public dialogRef: MatDialogRef<TodoDialog>,
              @Inject(MAT_DIALOG_DATA) public data: TodoDialogData) {
  }

  get todosFormArray(): FormArray {
    return this.todosForm.get('todos') as FormArray;
  }

  ngOnInit() {
    this.mode         = this.data.mode;
    this.dependencies = this.data.dependencies;
    this.types        = this.data.types;
    const index       = 0;
    const formGroup   = this.createTodoForm(this.data.todos[index], index);
    this.todosForm    = this.formBuilder.group({
      todos: new FormArray([formGroup])
    });
  }

  addTodo() {
    const index     = this.todosFormArray.controls.length;
    const formGroup = this.createTodoForm(null, index);
    this.todosFormArray.push(formGroup);
  }

  createTodoForm(todo: TODO, index: number): FormGroup {
    const formGroup = this.formBuilder.group({
      index:        [index],
      id:           [todo?.id],
      name:         [todo?.name        , [Validators.required]],
      type:         [todo?.type        , [Validators.required]],
      dependencies: [todo?.dependencies, []],
      description:  [todo?.description , []]
    });
    return formGroup;
  }

  deleteTodoForm(formIndex: number) {
    this.todosFormArray.removeAt(formIndex);
    this.todosFormArray.controls.forEach((formGroup, index) => {
      formGroup.get('index').setValue(index, {onlySelf: true, emitEvent: false});
    });
  }

  save(): void {
    this.dialogRef.close(this.todosFormArray.value);
  }
}
view raw
todo-dialog.ts 没有任何订阅
import {TODO} from '../todo.model';

export interface TodoDialogData {
  todos?: TODO[];
  dependencies: TODO[];
  types: string[];
  mode: string; // 'create' | 'update'
}
todo-dialog-data.model.ts

有什么不同?

这是最后一个优化步骤后的最终 Chrome DevTools 统计信息。在记录过程中,我执行了几乎相同的示例场景:三次使用对话框创建九个新的待办事项,添加和删除一些表单,单击一次对话框的取消按钮,并通过添加依赖项更新一个待办事项给它。

  • 在场景结束时,性能时间线记录中的 JS 堆大小为 ~9 MB 而不是 ~10 MB(将鼠标悬停在图表上即可查看)。
  • 在我们的第一个绩效记录中,文件数量是两个而不是七个。
  • 听众人数为 609 人,而不是 2,008 人。
  • 第二个快照中的 JS 堆增加了 0.8 MB——在创建了九个新的 todo 之后——而不是我们教程开始时的 3 MB。
  • 垃圾收集少了很多→缓解了频繁垃圾收集的问题。

这是有道理的,不是吗?

完整的 Angular 9 todo 应用程序可在此 GitHub 存储库中找到。

最后的想法

构建大型应用程序需要编写大量代码、复杂页面、长列表以及许多组件和模块。Angular 是一个在内存管理方面做得很好的框架。尽管如此,某些情况会导致错误,从而导致内存泄漏,从而影响用户体验。在它出现在生产中之前,我们不会知道可能是我们造成了这个问题。

用户重新加载页面的频率越来越低。当性能延迟超过一秒时,他们就会失去对正在执行的任务的关注。超过 10 秒,用户会感到沮丧,并可能放弃任务。他们以后可能会也可能不会回来。这就是为什么为长期会话保持最佳性能至关重要。

调试内存泄漏问题可能是一项艰巨的任务,避免它们需要对问题的认识和持续的警惕。

点赞收藏
trigkit4

曾就职于RingCentral、imToken,曾负责知名以太坊区块链钱包imToken去中心化交易所web端和RN端研发,现任笨马网络资深前端工程师,广泛涉猎各种前沿的前端领域,包括:前端工程化、性能优化、PWA,web3.0等领域,敏捷开发践行者,多年的React开发经验,喜欢钻研技术,擅长分析问题,解决问题。

请先登录,查看1条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步
2
1
Lv1
trigkit4

徽章

曾就职于RingCentral、imToken,曾负责知名以太坊区块链钱包imToken去中心化交易所web端和RN端研发,现任笨马网络资深前端工程师,广泛涉猎各种前沿的前端领域,包括:前端工程化、性能优化、PWA,web3.0等领域,敏捷开发践行者,多年的React开发经验,喜欢钻研技术,擅长分析问题,解决问题。