Sudut tanpa zone.js: kinerja maksimum

Pengembang sudut berutang banyak ke zone.js. Dia, misalnya, membantu mencapai kemudahan yang hampir ajaib dalam bekerja dengan Angular. Bahkan, hampir selalu, ketika Anda hanya perlu mengubah beberapa properti, dan kami mengubahnya tanpa memikirkan apa pun, Angular merender ulang komponen yang sesuai. Akibatnya, apa yang dilihat pengguna selalu berisi informasi terbaru. Ini bagus sekali.

Di sini saya ingin mengeksplorasi beberapa aspek tentang bagaimana penggunaan kompiler Ivy baru (yang muncul di Angular 9) dapat sangat membantu penolakan penggunaan zone.js.



Dengan meninggalkan perpustakaan ini, saya dapat secara signifikan meningkatkan kinerja aplikasi Angular yang berjalan di bawah beban berat. Pada saat yang sama, saya berhasil menerapkan mekanisme yang saya butuhkan menggunakan dekorator TypeScript, yang menyebabkan biaya tambahan yang sangat kecil dari sumber daya sistem.

Harap dicatat bahwa pendekatan untuk mengoptimalkan aplikasi Angular, yang disajikan dalam artikel ini, hanya dimungkinkan karena Angular Ivy dan AOT diaktifkan secara default. Artikel ini ditulis untuk tujuan pendidikan, tidak bertujuan mempromosikan pendekatan yang disajikan di dalamnya untuk pengembangan proyek Angular.

Mengapa Anda perlu menggunakan Angular tanpa zone.js?


Sebelum kita melanjutkan, mari kita ajukan satu pertanyaan penting: "Apakah layak menyingkirkan zone.js, mengingat perpustakaan ini membantu kita membuat kembali templat dengan sedikit usaha?" Tentu saja perpustakaan ini sangat bermanfaat. Tetapi, seperti biasa, Anda harus membayar semuanya.

Jika aplikasi Anda memiliki persyaratan kinerja tertentu, menonaktifkan zone.js dapat membantu memenuhi persyaratan itu. Contoh aplikasi di mana kinerja sangat penting adalah proyek yang antarmuka-nya sering diperbarui. Dalam kasus saya, proyek semacam itu ternyata merupakan aplikasi perdagangan waktu-nyata. Bagian kliennya secara konstan menerima pesan melalui protokol WebSocket. Data dari pesan-pesan ini harus ditampilkan secepat mungkin.

Hapus zone.js dari Angular


Sudut dapat dengan mudah dibuat untuk bekerja tanpa zone.js. Untuk melakukan ini, Anda harus terlebih dahulu mengomentari atau menghapus perintah impor yang sesuai, yang terletak di file polyfills.ts .


Mengomentari perintah impor zone.js

Berikutnya - Anda perlu melengkapi modul root dengan opsi berikut:

 platformBrowserDynamic()  .bootstrapModule(AppModule, {    ngZone: 'noop'  })  .catch(err => console.error(err)); 

Angular Ivy: Perubahan deteksi otomatis dengan ɵdetectChanges dan ɵmarkDirty


Sebelum kita dapat mulai membuat dekorator TypeScript, kita perlu belajar tentang bagaimana Ivy memungkinkan Anda untuk memohon proses mendeteksi perubahan komponen, membuatnya kotor, dan melewati zone.js dan DI.

Dua fungsi tambahan sekarang tersedia untuk kami, diekspor dari @angular/core . Ini adalah ɵdetectChanges dan ɵmarkDirty . Kedua fungsi ini masih ditujukan untuk penggunaan internal dan tidak stabil - simbol ɵ terletak di awal nama mereka.

Mari kita lihat cara menggunakan fitur-fitur ini.

▍ Fungsi ɵmarkDirty


Fungsi ini memungkinkan Anda untuk menandai komponen, membuatnya "kotor," yaitu, perlu dirender ulang. Dia, jika komponen itu tidak ditandai "kotor" sebelum dipanggil, berencana untuk memulai proses deteksi perubahan.

 import { ɵmarkDirty as markDirty } from '@angular/core'; @Component({...}) class MyComponent {  setTitle(title: string) {    this.title = title;    markDirty(this);  } } 

▍ ɵdetectChanges Function


Dokumentasi internal sudut mengatakan bahwa, untuk alasan kinerja, Anda tidak boleh menggunakan ɵdetectChanges . Sebagai gantinya, disarankan untuk menggunakan fungsi ɵmarkDirty . Fungsi ɵdetectChanges serempak memanggil proses mendeteksi perubahan dalam suatu komponen dan subkomponennya.

 import { ɵdetectChanges as detectChanges } from '@angular/core'; @Component({...}) class MyComponent {  setTitle(title: string) {    this.title = title;    detectChanges(this);  } } 

Secara otomatis mendeteksi perubahan menggunakan dekorator TypeScript


Meskipun fungsi-fungsi yang disediakan oleh Angular meningkatkan kegunaan pengembangan dengan membiarkan DI berputar, programmer masih bisa frustrasi oleh kenyataan bahwa ia perlu mengimpor dan memanggil fungsi-fungsi ini sendiri untuk memulai proses deteksi perubahan.

Untuk menyederhanakan dimulainya deteksi perubahan, Anda dapat menulis dekorator TypeScript, yang secara mandiri akan memecahkan masalah ini. Tentu saja, ada beberapa batasan di sini, yang akan kita bahas di bawah ini, tetapi dalam kasus saya pendekatan ini ternyata tepat seperti yang saya butuhkan.

▍ Memperkenalkan dekorator yang diobservasi @


Untuk mendeteksi perubahan, berusaha sesedikit mungkin, kami akan membuat dekorator yang dapat diterapkan dalam tiga cara. Yaitu, itu berlaku untuk entitas berikut:

  • Untuk metode sinkron.
  • Benda yang bisa diamati.
  • Untuk benda biasa.

Pertimbangkan beberapa contoh kecil. Dalam fragmen kode berikut, kami menerapkan dekorator @observed ke objek state dan ke metode changeTitle :

 export class Component {    title = '';    @observed() state = {        name: ''    };    @observed()    changeTitle(title: string) {        this.title = title;    }    changeName(name: string) {        this.state.name = name;    } } 

  • Untuk memeriksa perubahan pada objek state , kami menggunakan objek proxy yang memotong perubahan pada objek dan memanggil prosedur untuk mendeteksi perubahan.
  • Kami mengganti metode changeTitle dengan menerapkan fungsi yang pertama kali memanggil metode ini dan kemudian memulai proses deteksi perubahan.

Dan berikut ini adalah contoh dengan BehaviorSubject :

 export class AppComponent {    @observed() show$ = new BehaviorSubject(true);    toggle() {        this.show$.next(!this.show$.value);    } } 

Dalam hal objek yang Dapat Diobservasi, menggunakan dekorator terlihat sedikit lebih rumit. Yaitu, Anda perlu berlangganan objek yang diamati dan menandai komponen sebagai "kotor" dalam langganan, tetapi Anda juga harus menghapus langganan. Untuk melakukan ini, kami menetapkan kembali ngOnInit dan ngOnDestroy untuk berlangganan dan membersihkannya nanti.

▍Membuat dekorator


Berikut adalah tanda tangan dekorator yang observed :

 export function observed() {  return function(    target: object,    propertyKey: string,    descriptor?: PropertyDescriptor  ) {} } 

Seperti yang Anda lihat, descriptor adalah parameter opsional. Ini karena kita membutuhkan dekorator untuk diterapkan pada metode dan properti. Jika parameter ada, ini berarti bahwa dekorator diterapkan ke metode. Dalam hal ini, kami melakukan ini:

  • Simpan properti descriptor. value .
  • Kami mendefinisikan kembali metode sebagai berikut: panggil fungsi asli, lalu panggil markDirty(this) untuk memulai proses deteksi perubahan. Begini tampilannya:

     if (descriptor) {  const original = descriptor.value; //     descriptor.value = function(...args: any[]) {    original.apply(this, args); //       markDirty(this);  }; } else {  //   } 

Selanjutnya, Anda perlu memeriksa jenis properti apa yang kami hadapi. Ini bisa menjadi objek yang bisa diamati atau objek biasa. Di sini kita akan menggunakan API Angular internal lain. Saya percaya, ini tidak dimaksudkan untuk digunakan dalam aplikasi reguler (maaf!).

Kita berbicara tentang properti ɵcmp , yang memberikan akses ke properti yang diproses oleh Angular setelah didefinisikan. Kita dapat menggunakannya untuk mengganti metode onDestroy onInit dan onDestroy .

 const getCmp = type => (type).ɵcmp; const cmp = getCmp(target.constructor); const onInit = cmp.onInit || noop; const onDestroy = cmp.onDestroy || noop; 

Untuk menandai properti sebagai yang akan dimonitor, kami menggunakan ReflectMetadata dan menetapkan nilainya menjadi true . Akibatnya, kita akan tahu bahwa kita perlu mengamati properti ketika komponen diinisialisasi:

 Reflect.set(target, propertyKey, true); 

Sekarang saatnya untuk mengganti pengait onInit dan memeriksa properti ketika membuat instance komponen:

 cmp.onInit = function() {  checkComponentProperties(this);  onInit.call(this); }; 

Kami mendefinisikan fungsi checkComponentProperties , yang akan mem-bypass properti komponen, memfilternya sesuai dengan nilai yang ditetapkan sebelumnya menggunakan Reflect.set :

 const checkComponentProperties = (ctx) => {  const props = Object.getOwnPropertyNames(ctx);  props.map((prop) => {    return Reflect.get(target, prop);  }).filter(Boolean).forEach(() => {    checkProperty.call(ctx, propertyKey);  }); }; 

Fungsi checkProperty akan bertanggung jawab untuk mendekorasi properti individual. Pertama, kami memeriksa apakah properti itu objek yang Dapat Diamati atau biasa. Jika ini adalah objek yang dapat Diobservasi, kami berlangganan dan menambahkan langganan ke daftar langganan yang disimpan dalam komponen untuk kebutuhan internalnya.

 const checkProperty = function(name: string) {  const ctx = this;  if (ctx[name] instanceof Observable) {    const subscriptions = getSubscriptions(ctx);    subscriptions.add(ctx[name].subscribe(() => {      markDirty(ctx);    }));  } else {    //    } }; 

Jika properti adalah objek biasa, maka kami akan mengonversinya menjadi objek Proxy dan memanggil markDirty dalam fungsi handler :

 const handler = {  set(obj, prop, value) {    obj[prop] = value;    ɵmarkDirty(ctx);    return true;  } }; ctx[name] = new Proxy(ctx, handler); 

Terakhir, Anda perlu menghapus langganan setelah menghancurkan komponen:

 cmp.onDestroy = function() {  const ctx = this;  if (ctx[subscriptionsSymbol]) {    ctx[subscriptionsSymbol].unsubscribe();  }  onDestroy.call(ctx); }; 

Kemungkinan dekorator ini tidak dapat disebut komprehensif. Mereka tidak mencakup semua kemungkinan penggunaan yang mungkin muncul dalam aplikasi besar. Misalnya, ini adalah panggilan ke fungsi templat yang mengembalikan objek yang dapat diobservasi. Tapi saya sedang mengusahakannya.

Meskipun demikian, dekorator di atas sudah cukup untuk proyek kecil saya. Anda akan menemukan kode lengkapnya di akhir materi.

Analisis hasil percepatan aplikasi


Sekarang kita telah berbicara sedikit tentang mekanisme internal Ivy, dan bagaimana membuat dekorator menggunakan mekanisme ini, saatnya untuk menguji apa yang kita dapatkan dalam aplikasi nyata.

Saya, untuk mengetahui pengaruh menyingkirkan zone.js pada kinerja aplikasi Angular, menggunakan proyek hobi Cryptofolio saya.

Saya menerapkan dekorator ke semua tautan yang diperlukan yang digunakan dalam templat dan dinonaktifkan zone.js. Sebagai contoh, pertimbangkan komponen berikut:

 @Component({...}) export class AssetPricerComponent {  @observed() price$: Observable<string>;  @observed() trend$: Observable<Trend>;   // ... } 

Dua variabel digunakan dalam templat: price (harga aset akan ditempatkan di sini) dan trend (variabel ini dapat mengambil nilai up , stale dan down , menunjukkan arah perubahan harga). Saya menghiasnya dengan @observed .

▍ Ukuran bundel proyek


Untuk memulainya, mari kita lihat seberapa besar ukuran bundel proyek telah berkurang sementara menyingkirkan zone.js. Di bawah ini adalah hasil dari membangun proyek dengan zone.js.


Hasil membangun proyek dengan zone.js

Dan di sini adalah perakitan tanpa zone.js.


Hasil membangun proyek tanpa zone.js

Perhatikan polyfills-es2015.xxx.js . Jika proyek menggunakan zone.js, maka ukurannya sekitar 35 Kb. Tetapi tanpa zone.js - hanya 130 byte.

OotingBooting


Saya meneliti dua opsi aplikasi menggunakan Mercusuar. Hasil penelitian ini diberikan di bawah ini. Perlu dicatat bahwa saya tidak akan menganggapnya terlalu serius. Faktanya adalah ketika mencoba mencari nilai rata-rata, saya mendapat hasil yang sangat berbeda dengan melakukan beberapa pengukuran untuk versi aplikasi yang sama.

Mungkin perbedaan dalam mengevaluasi dua opsi aplikasi hanya tergantung pada ukuran bundel.

Jadi, inilah hasil yang diperoleh untuk aplikasi yang menggunakan zone.js.


Hasil analisis untuk aplikasi yang menggunakan zone.js

Dan inilah yang terjadi setelah menganalisis aplikasi di mana zone.js tidak digunakan.


Hasil analisis untuk aplikasi yang tidak menggunakan zone.js

▍ Kinerja


Dan sekarang kita sampai pada yang paling menarik. Ini adalah kinerja aplikasi yang berjalan di bawah beban. Kami ingin mempelajari tentang bagaimana perasaan prosesor ketika aplikasi menampilkan pembaruan harga untuk ratusan aset beberapa kali per detik.

Untuk memuat aplikasi, saya membuat 100 entitas yang menyediakan data bersyarat dengan harga yang berubah setiap 250 ms. Jika harga naik, itu ditampilkan dalam warna hijau. Jika dikurangi - merah. Semua ini dengan serius dapat memuat MacBook Pro saya.

Perlu dicatat bahwa ketika bekerja di sektor keuangan pada beberapa aplikasi yang dirancang untuk transmisi fragmen data frekuensi tinggi, saya telah menemukan situasi yang sama beberapa kali.

Untuk menganalisis perbedaan versi aplikasi menggunakan sumber daya prosesor, saya menggunakan alat pengembang Chrome.

Seperti apa tampilan aplikasi yang menggunakan zone.js.


Pemuatan sistem dibuat oleh aplikasi yang menggunakan zone.js

Dan inilah cara aplikasi bekerja di mana zone.js tidak digunakan.


Pemuatan sistem dibuat oleh aplikasi yang tidak menggunakan zone.js

Kami menganalisis hasil ini, memperhatikan grafik beban prosesor (kuning):

  • Seperti yang Anda lihat, aplikasi yang menggunakan zone.js terus-menerus memuat prosesor hingga 70-100%! Jika Anda menahan tab browser terbuka untuk waktu yang lama, membuat beban seperti itu pada sistem, maka aplikasi yang berjalan di dalamnya mungkin gagal.
  • Dan versi aplikasi di mana zone.js tidak digunakan menciptakan beban yang stabil pada prosesor dalam kisaran 30 hingga 40%. Hebat!

Harap perhatikan bahwa hasil ini diperoleh dengan jendela Alat Pengembang Chrome terbuka, yang juga membuat sistem tegang dan memperlambat aplikasi.

▍ peningkatan beban


Saya mencoba memastikan bahwa setiap entitas yang bertanggung jawab untuk memperbarui harga akan mengeluarkan 4 pembaruan lagi setiap detik selain apa yang sudah dihasilkannya.

Inilah yang berhasil kami ketahui tentang aplikasi di mana zone.js tidak digunakan:

  • Aplikasi ini biasanya diatasi dengan beban, sekarang menggunakan sekitar 50% dari sumber daya prosesor.
  • Dia berhasil memuat prosesor sebanyak aplikasi dengan zone.js, hanya ketika harga diperbarui setiap 10 ms (data baru, seperti sebelumnya, berasal dari 100 entitas).

▍ Analisis Kinerja dengan Benchpress Sudut


Analisis kinerja yang saya lakukan di atas tidak dapat disebut sangat ilmiah. Untuk studi yang lebih serius tentang kinerja berbagai kerangka kerja, saya akan merekomendasikan menggunakan tolok ukur ini . Untuk penelitian, Angular harus memilih versi biasa dari kerangka ini dan versinya tanpa zone.js.

Saya, terinspirasi oleh beberapa ide tolok ukur ini, menciptakan sebuah proyek yang melakukan perhitungan berat. Saya menguji kinerjanya dengan Angular Benchpress .

Berikut adalah kode komponen yang diuji:

 @Component({...}) export class AppComponent {  public data = [];  @observed()  run(length: number) {    this.clear();    this.buildData(length);  }  @observed()  append(length: number) {    this.buildData(length);  }  @observed()  removeAll() {    this.clear();  }  @observed()  remove(item) {    for (let i = 0, l = this.data.length; i < l; i++) {      if (this.data[i].id === item.id) {        this.data.splice(i, 1);        break;      }    }  }  trackById(item) {    return item.id;  }  private clear() {    this.data = [];  }  private buildData(length: number) {    const start = this.data.length;    const end = start + length;    for (let n = start; n <= end; n++) {      this.data.push({        id: n,        label: Math.random()      });    }  } } 

Saya meluncurkan satu set tolok ukur kecil menggunakan Protractor dan Benchpress. Operasi dilakukan beberapa kali.


Benchpress beraksi

Hasil


Berikut adalah contoh hasil yang diperoleh dengan menggunakan Benchpress.


Hasil Benchpress

Berikut ini penjelasan indikator yang disajikan dalam tabel ini:

  • gcAmount : volume operasi gc (pengumpulan sampah), Kb.
  • gcTime : waktu operasi gc, ms.
  • majorGcTime : waktu operasi utama gc, ms.
  • pureScriptTime : waktu eksekusi skrip dalam ms, tidak termasuk operasi gc dan rendering.
  • renderTime : waktu render, ms.
  • scriptTime : waktu pelaksanaan skrip dengan mempertimbangkan operasi dan rendering gc akun.

Sekarang kita akan mempertimbangkan analisis kinerja beberapa operasi di berbagai varian aplikasi. Hijau menunjukkan hasil aplikasi yang menggunakan zone.js, oranye menunjukkan hasil aplikasi tanpa zone.js. Harap dicatat bahwa hanya waktu rendering dianalisis di sini. Jika Anda tertarik dengan semua hasil tes, periksa di sini .

Uji: membuat 1000 baris


Dalam tes pertama, 1000 baris dibuat.


Hasil tes

Uji: membuat 10.000 baris


Seiring bertambahnya beban pada aplikasi, begitu pula perbedaan dalam kinerjanya.


Hasil tes

Uji: gabung 1000 baris


Dalam tes ini, 1000 baris ditambahkan ke 10.000 baris.


Hasil tes

Uji: menghapus 10.000 baris


Di sini, 10.000 baris dibuat, yang kemudian dihapus.


Hasil tes

Kode Sumber Dekorator TypeScript


Di bawah ini adalah kode sumber dekorator TypeScript yang dibahas di sini. Kode ini juga dapat ditemukan di sini .

 // tslint:disable import { Observable, Subscription } from 'rxjs'; import { Type, ɵComponentType as ComponentType, ɵmarkDirty as markDirty } from '@angular/core'; interface ComponentDefinition {  onInit(): void;  onDestroy(): void; } const noop = () => { }; const getCmp = <T>(type: Function) => (type as any).ɵcmp as ComponentDefinition; const subscriptionsSymbol = Symbol('__ng__subscriptions'); export function observed() {  return function(    target: object,    propertyKey: string,    descriptor?: PropertyDescriptor  ) {    if (descriptor) {      const original = descriptor.value;      descriptor.value = function(...args: any[]) {        original.apply(this, args);        markDirty(this);      };    } else {      const cmp = getCmp(target.constructor);      if (!cmp) {        throw new Error(`Property ɵcmp is undefined`);      }      const onInit = cmp.onInit || noop;      const onDestroy = cmp.onDestroy || noop;      const getSubscriptions = (ctx) => {        if (ctx[subscriptionsSymbol]) {          return ctx[subscriptionsSymbol];        }        ctx[subscriptionsSymbol] = new Subscription();        return ctx[subscriptionsSymbol];      };      const checkProperty = function(name: string) {        const ctx = this;        if (ctx[name] instanceof Observable) {          const subscriptions = getSubscriptions(ctx);          subscriptions.add(ctx[name].subscribe(() => markDirty(ctx)));        } else {          const handler = {            set(obj: object, prop: string, value: unknown) {              obj[prop] = value;              markDirty(ctx);              return true;            }          };          ctx[name] = new Proxy(ctx, handler);        }      };      const checkComponentProperties = (ctx) => {        const props = Object.getOwnPropertyNames(ctx);        props.map((prop) => {          return Reflect.get(target, prop);        }).filter(Boolean).forEach(() => {          checkProperty.call(ctx, propertyKey);        });      };      cmp.onInit = function() {        const ctx = this;        onInit.call(ctx);        checkComponentProperties(ctx);      };      cmp.onDestroy = function() {        const ctx = this;        onDestroy.call(ctx);        if (ctx[subscriptionsSymbol]) {          ctx[subscriptionsSymbol].unsubscribe();        }      };      Reflect.set(target, propertyKey, true);    }  }; } 

Ringkasan


Meskipun saya berharap Anda menyukai cerita saya tentang mengoptimalkan kinerja proyek Angular, saya juga berharap bahwa saya tidak akan membuat Anda terburu-buru untuk menghapus zone.js dari proyek Anda. Strategi yang dijelaskan di sini harus menjadi pilihan terakhir yang dapat Anda gunakan untuk meningkatkan kinerja aplikasi Angular Anda.

Pertama, Anda perlu mencoba pendekatan seperti menggunakan strategi deteksi perubahan OnPush, menerapkan trackBy , menonaktifkan komponen, mengeksekusi kode di luar zone.js, peristiwa blacklisting zone.js (daftar metode optimasi ini dapat dilanjutkan). Pendekatan yang diperlihatkan di sini cukup mahal, dan saya tidak yakin semua orang mau membayar dengan harga tinggi untuk kinerja.

Bahkan, pengembangan tanpa zone.js mungkin bukan hal yang paling menarik. Mungkin ini tidak hanya untuk mereka yang terlibat dalam proyek di bawah kendali penuhnya. Artinya - itu adalah pemilik dependensi dan memiliki kemampuan dan waktu untuk membawa semuanya ke bentuk yang semestinya.

Jika ternyata Anda telah mencoba segalanya dan percaya bahwa hambatan proyek Anda adalah zone.js, maka mungkin Anda harus mencoba mempercepat Angular dengan mendeteksi perubahan secara independen.

Saya harap artikel ini memungkinkan Anda untuk melihat apa yang diharapkan Angular di masa depan, apa yang mampu dilakukan Ivy, dan apa yang dapat dilakukan dengan zone.js untuk memaksimalkan kecepatan aplikasi.

Pembaca yang budiman! Bagaimana Anda mengoptimalkan proyek Sudut Anda yang membutuhkan kinerja maksimal?


Source: https://habr.com/ru/post/id476956/


All Articles