تشخیص تغییر Angular را بهینه کنید

برای تجربه کاربری بهتر، تشخیص تغییرات سریعتر را پیاده سازی کنید.

Angular مکانیسم تشخیص تغییر خود را به صورت دوره ای اجرا می کند تا تغییرات مدل داده در نمای برنامه منعکس شود. تشخیص تغییر می تواند به صورت دستی یا از طریق یک رویداد ناهمزمان (به عنوان مثال، تعامل کاربر یا تکمیل XHR) فعال شود.

تشخیص تغییر ابزار قدرتمندی است، اما اگر اغلب اجرا شود، می‌تواند محاسبات زیادی را راه‌اندازی کند و رشته اصلی مرورگر را مسدود کند.

در این پست، نحوه کنترل و بهینه سازی مکانیسم تشخیص تغییر را با پرش از بخش هایی از برنامه خود و اجرای تشخیص تغییر تنها در صورت لزوم، خواهید آموخت.

تشخیص تغییر Angular در داخل

برای درک نحوه عملکرد تشخیص تغییر Angular، بیایید به یک برنامه نمونه نگاه کنیم!

می توانید کد برنامه را در این مخزن GitHub پیدا کنید.

این برنامه کارمندان دو بخش در یک شرکت - فروش و تحقیق و توسعه - را فهرست می کند و دارای دو جزء است:

  • AppComponent که جزء اصلی برنامه است و
  • دو نمونه از EmployeeListComponent ، یکی برای فروش و دیگری برای تحقیق و توسعه.

نمونه برنامه

می توانید دو نمونه EmployeeListComponent را در قالب AppComponent مشاهده کنید:

<app-employee-list
  [data]="salesList"
  department="Sales"
  (add)="add(salesList, $event)"
  (remove)="remove(salesList, $event)"
></app-employee-list>

<app-employee-list
  [data]="rndList"
  department="R&D"
  (add)="add(rndList, $event)"
  (remove)="remove(rndList, $event)"
></app-employee-list>

برای هر کارمند یک نام و یک مقدار عددی وجود دارد. این برنامه مقدار عددی کارمند را به یک محاسبه تجاری ارسال می کند و نتیجه را روی صفحه نمایش می دهد.

اکنون نگاهی به EmployeeListComponent بیندازید:

const fibonacci = (num: number): number => {
  if (num === 1 || num === 2) {
    return 1;
  }
  return fibonacci(num - 1) + fibonacci(num - 2);
};

@Component(...)
export class EmployeeListComponent {
  @Input() data: EmployeeData[];
  @Input() department: string;
  @Output() remove = new EventEmitter<EmployeeData>();
  @Output() add = new EventEmitter<string>();

  label: string;

  handleKey(event: any) {
    if (event.keyCode === 13) {
      this.add.emit(this.label);
      this.label = '';
    }
  }

  calculate(num: number) {
    return fibonacci(num);
  }
}

EmployeeListComponent لیستی از کارکنان و نام بخش را به عنوان ورودی می پذیرد. هنگامی که کاربر سعی می کند یک کارمند را حذف یا اضافه کند، جزء خروجی مربوطه را راه اندازی می کند. مؤلفه همچنین روش calculate را تعریف می کند که محاسبات تجاری را پیاده سازی می کند.

در اینجا الگوی EmployeeListComponent آمده است:

<h1 title="Department">{{ department }}</h1>
<mat-form-field>
  <input placeholder="Enter name here" matInput type="text" [(ngModel)]="label" (keydown)="handleKey($event)">
</mat-form-field>
<mat-list>
  <mat-list-item *ngFor="let item of data">
    <h3 matLine title="Name">
      {{ item.label }}
    </h3>
    <md-chip title="Score" class="mat-chip mat-primary mat-chip-selected" color="primary" selected="true">
      {{ calculate(item.num) }}
    </md-chip>
  </mat-list-item>
</mat-list>

این کد روی همه کارمندان لیست تکرار می شود و برای هر یک، یک آیتم لیست را ارائه می دهد. همچنین شامل یک دستورالعمل ngModel برای اتصال داده دو طرفه بین ورودی و ویژگی label اعلام شده در EmployeeListComponent است.

با دو نمونه EmployeeListComponent ، برنامه درخت مؤلفه زیر را تشکیل می دهد:

درخت جزء

AppComponent جزء اصلی برنامه است. اجزای فرزند آن دو نمونه از EmployeeListComponent هستند. هر نمونه دارای فهرستی از موارد (E 1 ، E 2 ، و غیره) است که نشان دهنده تک تک کارمندان در بخش است.

هنگامی که کاربر شروع به وارد کردن نام یک کارمند جدید در کادر ورودی در یک EmployeeListComponent می کند، Angular تشخیص تغییر را برای کل درخت مؤلفه شروع می کند که از AppComponent شروع می شود. این بدان معنی است که در حالی که کاربر در حال تایپ متن ورودی است، Angular به طور مکرر مقادیر عددی مرتبط با هر کارمند را مجدداً محاسبه می کند تا تأیید کند که از آخرین بررسی تغییری نکرده است.

برای اینکه ببینید چقدر این می تواند کند باشد، نسخه غیربهینه پروژه را در StackBlitz باز کنید و نام کارمند را وارد کنید.

با راه‌اندازی پروژه نمونه و باز کردن برگه Performance در Chrome DevTools، می‌توانید تأیید کنید که کاهش سرعت ناشی از تابع fibonacci است.

  1. «Control+Shift+J» (یا «Command+Option+J» در Mac) را فشار دهید تا DevTools باز شود.
  2. روی تب Performance کلیک کنید.

حالا روی Record کلیک کنید (در گوشه سمت چپ بالای پانل Performance ) و شروع به تایپ در یکی از کادرهای متنی در برنامه کنید. در چند ثانیه روی Record کلیک کنید دوباره برای توقف ضبط هنگامی که Chrome DevTools تمام داده‌های نمایه‌ای را که جمع‌آوری کرده است پردازش می‌کند، چیزی شبیه به این خواهید دید:

پروفایل عملکرد

اگر تعداد زیادی کارمند در لیست وجود داشته باشد، این فرآیند ممکن است رشته رابط کاربری مرورگر را مسدود کند و باعث افت فریم شود که منجر به تجربه کاربری بدی می شود.

رد شدن از زیردرخت های جزء

هنگامی که کاربر در حال تایپ متن ورودی برای فروش EmployeeListComponent است، می‌دانید که داده‌های بخش R&D تغییر نمی‌کند—بنابراین دلیلی برای اجرای تشخیص تغییر در مؤلفه آن وجود ندارد. برای اطمینان از اینکه نمونه تحقیق و توسعه شناسایی تغییر را فعال نمی کند، changeDetectionStrategy of EmployeeListComponent را روی OnPush قرار دهید:

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

@Component({
  selector: 'app-employee-list',
  template: `...`,
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrls: ['employee-list.component.css']
})
export class EmployeeListComponent {...}

اکنون وقتی کاربر یک ورودی متنی را تایپ می کند، تشخیص تغییر فقط برای بخش مربوطه فعال می شود:

تشخیص تغییر در زیردرخت جزء

می‌توانید این بهینه‌سازی اعمال شده برای برنامه اصلی را در اینجا پیدا کنید.

می توانید اطلاعات بیشتری در مورد استراتژی تشخیص تغییر OnPush در اسناد رسمی Angular بخوانید.

برای مشاهده تاثیر این بهینه سازی، یک کارمند جدید را در برنامه StackBlitz وارد کنید.

استفاده از لوله های خالص

حتی اگر استراتژی تشخیص تغییر برای EmployeeListComponent اکنون روی OnPush تنظیم شده است، Angular همچنان مقدار عددی را برای همه کارکنان یک بخش، زمانی که کاربر در ورودی متن مربوطه تایپ می‌کند، دوباره محاسبه می‌کند.

برای بهبود این رفتار می توانید از لوله های خالص استفاده کنید. لوله های خالص و ناخالص هر دو ورودی ها را می پذیرند و نتایجی را برمی گردند که می توانند در قالب استفاده شوند. تفاوت بین این دو این است که یک لوله خالص تنها در صورتی نتیجه خود را دوباره محاسبه می کند که ورودی متفاوتی از فراخوانی قبلی خود دریافت کند.

به یاد داشته باشید که برنامه با استفاده از روش calculate تعریف شده در EmployeeListComponent ، مقداری را برای نمایش بر اساس مقدار عددی کارمند محاسبه می‌کند. اگر محاسبه را به یک لوله خالص منتقل کنید، Angular فقط زمانی که آرگومان های آن تغییر کند عبارت pipe را دوباره محاسبه می کند. چارچوب تعیین می کند که آیا آرگومان های لوله با انجام یک بررسی مرجع تغییر کرده است یا خیر. این بدان معنی است که Angular هیچ گونه محاسبه مجددی را انجام نمی دهد مگر اینکه مقدار عددی یک کارمند به روز شود.

در اینجا نحوه انتقال محاسبات تجاری به لوله ای به نام CalculatePipe آورده شده است:

import { Pipe, PipeTransform } from '@angular/core';

const fibonacci = (num: number): number => {
  if (num === 1 || num === 2) {
    return 1;
  }
  return fibonacci(num - 1) + fibonacci(num - 2);
};

@Pipe({
  name: 'calculate'
})
export class CalculatePipe implements PipeTransform {
  transform(val: number) {
    return fibonacci(val);
  }
}

روش transform لوله تابع fibonacci را فراخوانی می کند. توجه کنید که لوله خالص است. Angular تمام لوله ها را خالص در نظر می گیرد مگر اینکه شما خلاف آن را مشخص کنید.

در نهایت، عبارت داخل قالب را برای EmployeeListComponent به روز کنید:

<mat-chip-list>
  <md-chip>
    {{ item.num | calculate }}
  </md-chip>
</mat-chip-list>

همین! اکنون وقتی کاربر ورودی متن مرتبط با هر بخش را تایپ می کند، برنامه مقدار عددی را برای تک تک کارمندان دوباره محاسبه نمی کند.

در برنامه زیر می توانید ببینید که تایپ چقدر راحت تر است!

برای مشاهده تاثیر آخرین بهینه سازی این مثال را در StackBlitz امتحان کنید .

کد با بهینه سازی لوله خالص برنامه اصلی در اینجا موجود است.

نتیجه گیری

هنگام مواجهه با کاهش سرعت اجرا در یک برنامه Angular:

  1. برنامه را با Chrome DevTools نمایه کنید تا ببینید کاهش سرعت از کجا می‌آید.
  2. استراتژی تشخیص تغییر OnPush را برای هرس کردن زیردرخت های یک جزء معرفی کنید.
  3. محاسبات سنگین را به لوله‌های خالص منتقل کنید تا چارچوب بتواند مقادیر محاسبه‌شده را در حافظه پنهان انجام دهد.