12/08/2018, 15:56

Using Zones in Angular for better performance

Trong một số bài trước, chúng ta đã nói về cách cải thiện hiệu năng ứng dụng Angular bằng cách khám phá các API DetectionStrategy của Angular.Trong khi chúng ta đã nghiên cứu nhiều tùy chọn khác nhau để cải thiện hiệu suất, hiệu năng của ứng dụng, chúng ta chắc chắn đã không nói về tất cả các tùy ...

Trong một số bài trước, chúng ta đã nói về cách cải thiện hiệu năng ứng dụng Angular bằng cách khám phá các API DetectionStrategy của Angular.Trong khi chúng ta đã nghiên cứu nhiều tùy chọn khác nhau để cải thiện hiệu suất, hiệu năng của ứng dụng, chúng ta chắc chắn đã không nói về tất cả các tùy chọn có thể.

Đó là lý do tại sao Jordi Collell chỉ ra rằng một tùy chọn khác sẽ tận dụng các API của Zone, để thực thi mã của chúng ta ở ngoài Angular Zone, điều này sẽ ngăn không cho Angular thực thi những task phát hiện được là không cần thiết.Ông thậm chí còn dành thời gian và năng lượng để tạo ra một ứng dụng Plunk demo cho thấy làm thế nào để làm chính xác điều đó.

Chúng tôi muốn nói cảm ơn vì sự đóng góp của ông và nghĩ rằng giải pháp ông đã đưa ra xứng đáng với bài viết của riêng mình.Vì vậy, trong bài này, chúng ta sẽ khám phá ra những giải pháp mới và giải thích cách Jordi sử dụng Zones để làm cho ứng dụng demo của chúng ta thực hiện ở gần 60 fps.

Table of contents

  • Seeing it in action
  • The idea of Zones
  • Running outside Angular’s Zone
  • Measuring the performance
  • Conclusion

Seeing it in action.

Trước khi chúng ta nhảy ngay vào code, trước tiên chúng ta hãy xem demo plunk với các ứng dụng đang chạy.Ý tưởng là để render 10.000 boxes SVG. Việc hiển thị 10.000 boxes không phải là một nhiệm vụ siêu phức tạp, tuy nhiên, thách thức nằm ở việc làm cho trải nghiệm drag trở nên trơn tru nhất có thể. Nói cách khác, chúng tôi nhắm tới mục tiêu 60 fps (khung / giây), điều này thực sự là thách thức lớn, xem xét Angular sẽ hiển thị lại tất cả 10.000 boxes khi sự kiện đã kích hoạt.

Demo có tại link sau: Demo using zone

Mặc dù sự khác biệt là khá tinh tế, phiên bản được tối ưu hóa thực hiện tốt hơn nhiều về thực thi JavaScript trên mỗi khung.Chúng ta sẽ xem xét một số sau đó, nhưng chúng ta hãy nhanh chóng recap các zone, sau đó đi sâu vào đoạn code và thảo luận cách Jordi sử dụng các API NgZone của Angular để đạt được hiệu suất này trước tiên.

The idea of Zones.

Trước khi chúng ta có thể sử dụng các API của Zone và đặc biệt là các ứng dụng từ NgZone của Angular, chúng ta cần phải hiểu rõ Zone thực sự là gì và cách chúng hữu ích trong thế giới Angular.Chúng ta sẽ không đi sâu vào chi tiết ở đây vì chúng ta có thể tham khảo hai bài viết sau đây:

Understanding Zones - Thảo luận khái niệm về zone nói chung và cách chúng có thể được sử dụng ví dụ: Thực hiện mã không đồng bộ cấu hình Zones in Angular - Tìm hiểu cách các API được sử dụng trong Angular để tạo ra một custom NgZone, cho phép người dùng và chính Angular chạy mã bên trong hoặc bên ngoài Angular Zone đó.

Nếu bạn chưa đọc những bài viết này, chúng tôi khuyên bạn nên thực hiện như vậy vì họ đã hiểu rõ về Zone và những gì họ làm.Điểm mấu chốt là, Zones wrap các API trình duyệt không đồng bộ, và thông báo cho người dùng khi một tác vụ không đồng bộ đã bắt đầu hoặc kết thúc.Angular lợi dụng các API này để nhận thông báo khi có bất kỳ tác vụ không đồng bộ nào được thực hiện.Nó bao gồm những thứ như call XHR, setTimeout(), và tuyệt vời hơn nhiều các user events như click, submit, mousedown, ..

Một khi được thông báo, Angular biết rằng nó phải thực hiện việc dò tìm thay đổi vì bất kỳ hoạt động không đồng bộ nào có thể đã thay đổi trạng thái ứng dụng.Điều này ví dụ luôn luôn là trường hợp khi chúng ta sử dụng dịch vụ Http của Angular để lấy dữ liệu từ một server từ ở xa.Đoạn code sau đây cho thấy cách gọi như vậy có thể thay đổi trạng thái ứng dụng:

@Component(...)
export class AppComponent {

  data: any; // initial application state

  constructor(private dataService: DataService) {}

  ngOnInit() {
    this.dataService.fetchDataFromRemoteService().subscribe(data => {
      this.data = data // application state has changed, change detection needs to run now
    });
  }
}

Điều rất hay ở đây là chúng ta cũng như các nhà phát triển không cần phải quan tâm đến việc thông báo cho Angular để thực hiện tìm kiếm thay đổi, bởi vì các Zone sẽ làm điều đó.Okay, bây giờ chúng ta đã đề cập đến vấn đề đó, chúng ta hãy cùng xem cách chúng có thể được sử dụng để làm cho ứng dụng demo của chúng ta nhanh hơn.

Running outside Angular’s Zone

Chúng ta biết rằng phát hiện thay đổi được thực hiện bất cứ khi nào một sự kiện không đồng bộ xảy ra và trình xử lý sự kiện bị ràng buộc với sự kiện đó.Đây chính là lý do tại sao bản demo ban đầu của chúng ta thực hiện.Hãy xem template của AppComponent:

@Component({
  ...
  template: `
    <svg (mousedown)="mouseDown($event)"
         (mouseup)="mouseUp($event)"
         (mousemove)="mouseMove($event)">

      <svg:g box *ngFor="let box of boxes" [box]="box">
      </svg:g>

    </svg>
  `
})
class AppComponent {
  ...
}

Ba bộ xử lý sự kiện được ràng buộc với phần tử SVG bên ngoài.Khi bất kỳ sự kiện nào xảy ra và các trình xử lý của chúng ta đã được thực thi thì sự dò tìm thay đổi được thực hiện.Trên thực tế, điều này có nghĩa là Angular phát hiện sự thay đổi, ngay cả khi chúng ta chỉ di chuyển con chuột qua các boxes mà không thực sự kéo một box duy nhất!

Đây là nơi lợi dụng các API có ích của NgZone.NgZone cho phép chúng ta chạy một cách rõ ràng code bên ngoài Angular Zone, ngăn Angular chạy bất kỳ sự phát hiện thay đổi nào.Vì vậy về cơ bản, bộ xử lý vẫn sẽ được thực hiện, nhưng vì chúng ta sẽ không chạy trong Angular Zone, Angular sẽ không nhận được thông báo rằng một tác vụ đã được thực hiện và do đó không phát hiện thay đổi nào được thực hiện.Chúng ta chỉ muốn phát hiện sự thay đổi khi chúng ta thả hộp chúng ta đang kéo.

Okay, vậy làm thế nào để chúng ta đạt được điều này? Trong bài viết về Zones in Angular, chúng ta đã thảo luận cách thực thi code bên ngoài Angular Zone bằng NgZone.runOutsideAngular().Tất cả những gì chúng ta phải làm là đảm bảo rằng trình xử lý sự kiện mouseMove() chỉ được gắn và thực thi bên ngoài Angular Zone.Thêm vào đó, chúng ta biết rằng chúng ta muốn đính kèm xử lý sự kiện đó chỉ khi một hộp đang được chọn để kéo. Nói cách khác, chúng ta cần thay đổi trình xử lý sự kiện mouseDown() của chúng ta để bổ sung thêm sự kiện đó vào tài liệu.

Khi đó chúng ta sẽ làm như sau:

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

@Component(...)
export class AppComponent {
  ...
  element: HTMLElement;

  constructor(private zone: NgZone) {}

  mouseDown(event) {
    ...
    this.element = event.target;

    this.zone.runOutsideAngular(() => {
      window.document.addEventListener('mousemove', this.mouseMove.bind(this));
    });
  }

  mouseMove(event) {
    event.preventDefault();
    this.element.setAttribute('x', event.clientX + this.clientX + 'px');
    this.element.setAttribute('y', event.clientX + this.clientY + 'px');
  }
}

Chúng ta inject NgZone và call runOutsideAngular() bên trong trình xử lý sự kiện mouseDown() của chúng ta, trong đó chúng ta đính kèm một trình xử lý sự kiện cho sự kiện mousemove.Điều này đảm bảo rằng trình xử lý sự kiện mousemove thực sự chỉ được gắn vào tài liệu khi một hộp đang được chọn.Ngoài ra, chúng ta lưu một tham chiếu đến phần tử DOM bên dưới của hộp được nhấp để chúng ta có thể cập nhật các thuộc tính x và y của nó trong phương thức mouseMove().Chúng ta đang làm việc với phần tử DOM thay vì đối tượng hộp với các ràng buộc cho x và y, vì các ràng buộc sẽ không bị thay đổi vì chúng ta đang chạy mã bên ngoài Angular Zone.Nói cách khác, chúng ta cập nhật DOM, vì vậy chúng ta có thể thấy hộp đang di chuyển, nhưng chúng ta không thực sự cập nhật mô hình hộp (box model).

Ngoài ra, lưu ý rằng chúng ta đã loại bỏ các mouseMove() ràng buộc từ template của component's của chúng ta.Chúng ta cũng có thể loại bỏ các trình xử lý mouseUp() và đính kèm nó một cách bắt buộc, giống như chúng ta đã làm với mouseMove() handler.Tuy nhiên, nó sẽ không thêm bất kỳ giá trị hiệu suất nào, vì vậy chúng ta quyết định giữ nó trong template cho sự đơn giản của sake:

<svg (mousedown)="mouseDown($event)"
      (mouseup)="mouseUp($event)">

  <svg:g box *ngFor="let box of boxes" [box]="box">
  </svg:g>

</svg>

Trong bước tiếp theo, chúng ta muốn đảm bảo rằng, bất cứ khi nào chúng realease một box (mouseUp), chúng ta cập nhật box template, thêm vào đó chúng ta muốn thực hiện phát hiện thay đổi để mô hình đồng bộ với lần xem lại.Điều thú vị về NgZone không chỉ là nó cho phép chúng ta chạy code bên ngoài Angular zone, mà còn đi kèm với các API để chạy code bên trong Angular Zone, điều này sẽ khiến Angular thực hiện lại việc phát hiện thay đổi.Tất cả những gì chúng ta phải làm là gọi NgZone.run() và cho nó code cần được thực thi.

Dưới đây là code khi đã update mouseUp() event handler:

@Component(...)
export class AppComponent {
  ...
  mouseUp(event) {
    // Run this code inside Angular's Zone and perform change detection
    this.zone.run(() => {
      this.updateBox(this.currentId, event.clientX + this.offsetX, event.clientY + this.offsetY);
      this.currentId = null;
    });

    window.document.removeEventListener('mousemove', this.mouseMove);
  }
}

Cũng lưu ý rằng chúng ta đang xóa trình event listener cho sự kiện mousemove trên mỗi mouseUp.Nếu không, xử lý sự kiện vẫn sẽ được thực hiện trên mỗi lần di chuyển chuột.Nói cách khác, box sẽ tiếp tục di chuyển ngay cả sau khi ngón tay được nâng lên, chủ yếu là lấy phần drop ra khỏi drop and drag.Ngoài ra, chúng ta sẽ tích lũy xử lý sự kiện, mà có thể không chỉ gây ra các phản ứng phụ lạ mà còn làm tăng lên bộ nhớ thời gian chạy của chúng ta.

Measuring the performance

Okay, bây giờ chúng ta biết Jordi đã triển khai phiên bản demo này như thế nào, chúng ta hãy cùng xem xét một số con số.Những con số sau đây đã được ghi lại bằng cách sử dụng cùng một kỹ thuật chính xác trên cùng một máy như trong bài viết trước của chúng ta về hiệu suất previous article on performance.

  • 1st Profile, Event (mousemove): ~0.45ms, ~0.50ms (fastest, slowest)
  • 2nd Profile, Event (mousemove): ~0.39ms, ~0.52ms (fastest, slowest)
  • 3rd Profile, Event (mousemove): ~0.38ms, ~0.45ms (fastest, slowest)

Sử dụng các zone là một cách tuyệt vời để thoát khỏi sự phát hiện thay đổi của Angular, mà không cần tách detector thay đổi và làm cho mã ứng dụng quá phức tạp.Trên thực tế, API của Zones rất dễ sử dụng, đặc biệt là các API của NgZone để chạy code bên ngoài hoặc bên trong Angular.Dựa trên số liệu, chúng ta thậm chí có thể nói rằng phiên bản này nhanh như giải pháp nhanh nhất chúng ta đưa ra trong bài viết trước của chúng ta.Xem xét rằng trải nghiệm của nhà phát triển tốt hơn khi sử dụng các API zone, vì chúng dễ sử dụng hơn việc tách và gắn lại các tham chiếu của bộ dò thay đổi theo cách thủ công, đây chắc chắn là cải tiến hiệu suất tốt nhất mà chúng ta có cho đến nay.

Tuy nhiên, chúng ta không nên quên rằng giải pháp này cũng đi kèm với một vài sự cố .Ví dụ: chúng ta đang dựa vào các API DOM và đối tượng global windows, điều này là điều chúng ta nên tránh.Nếu chúng ta muốn sử dụng code này với phía server thì truy cập trực tiếp biến windows sẽ là vấn đề.Chúng ta sẽ thảo luận các vấn đề cụ thể ở phía server trong một bài viết trong tương lai.Mặc dầu vậy, đây cũng không phải là một vấn đề quá lớn.

Conclusion

Sử dụng Zones là một cách tuyệt vời để thoát khỏi sự phát hiện thay đổi của Angular, mà không cần tách detector thay đổi và làm cho mã ứng dụng quá phức tạp.Trên thực tế, API của Zones rất dễ sử dụng, đặc biệt là các API của NgZone để chạy mã bên ngoài hoặc bên trong Angular.Dựa trên số liệu, chúng ta thậm chí có thể nói rằng phiên bản này nhanh như giải pháp nhanh nhất chúng ta đưa ra trong bài viết trước của chúng ta.Xem xét rằng trải nghiệm của nhà phát triển tốt hơn khi sử dụng các Zones API , vì chúng dễ sử dụng hơn việc tách và gắn lại các tham chiếu của bộ dò thay đổi theo cách thủ công, đây chắc chắn là cải tiến hiệu suất tốt nhất mà chúng ta có cho đến nay.

Tuy nhiên, chúng ta không nên quên rằng giải pháp này cũng đi kèm với một vài sự cố .Ví dụ như chúng ta đang dựa vào các API DOM và đối tượng global word, điều này là điều chúng ta nên tránh.Nếu chúng ta muốn sử dụng mã này với phía server thì truy cập trực tiếp biến windows sẽ là vấn đề.Chúng ta sẽ thảo luận các vấn đề cụ thể ở phía server trong một bài viết trong tương lai.Vì vấn đề của bản demo này, đây không phải là một vấn đề quá lớn.

Nguồn bài viết và demo xem tại: Using Zones in Angular for better performance

0