Angular sin zone.js: máximo rendimiento

Los desarrolladores angulares le deben mucho a zone.js. Ella, por ejemplo, ayuda a lograr una facilidad casi mágica al trabajar con Angular. De hecho, casi siempre, cuando solo necesita cambiar alguna propiedad, y la cambiamos sin pensar en nada, Angular vuelve a representar los componentes correspondientes. Como resultado, lo que ve el usuario siempre contiene la información más reciente. Esto es simplemente genial.

Aquí me gustaría explorar algunos aspectos de cómo el uso del nuevo compilador Ivy (que apareció en Angular 9) puede facilitar en gran medida el rechazo del uso de zone.js.



Al abandonar esta biblioteca, pude aumentar significativamente el rendimiento de la aplicación Angular que se ejecuta bajo una gran carga. Al mismo tiempo, logré implementar los mecanismos que necesitaba usando decoradores TypeScript, lo que condujo a muy pocos recursos adicionales del sistema.

Tenga en cuenta que el enfoque para optimizar las aplicaciones angulares, presentado en este artículo, es posible solo porque Angular Ivy y AOT están habilitados de forma predeterminada. Este artículo está escrito con fines educativos, no tiene como objetivo promover el enfoque presentado para el desarrollo de proyectos angulares.

¿Por qué podría necesitar usar Angular sin zone.js?


Antes de continuar, hagamos una pregunta importante: "¿Vale la pena deshacerse de zone.js, dado que esta biblioteca nos ayuda a volver a renderizar plantillas con poco esfuerzo?" Por supuesto, esta biblioteca es muy útil. Pero, como siempre, tienes que pagar por todo.

Si su aplicación tiene requisitos de rendimiento específicos, deshabilitar zone.js puede ayudar a cumplir esos requisitos. Un ejemplo de una aplicación en la que el rendimiento es crucial es un proyecto cuya interfaz se actualiza con mucha frecuencia. En mi caso, dicho proyecto resultó ser una aplicación comercial en tiempo real. Su parte del cliente recibe constantemente mensajes a través del protocolo WebSocket. Los datos de estos mensajes deben mostrarse lo más rápido posible.

Eliminar zone.js de Angular


Angular se puede hacer que funcione muy fácilmente sin zone.js. Para hacer esto, primero debe comentar o eliminar el comando de importación correspondiente, que se encuentra en el archivo polyfills.ts .


Comentó el comando de importación zone.js

A continuación, debe equipar el módulo raíz con las siguientes opciones:

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

Hiedra angular: cambios de autodetección con ɵdetectChanges y ɵmarkDirty


Antes de que podamos comenzar a crear un decorador TypeScript, debemos aprender cómo Ivy le permite invocar el proceso de detectar cambios en los componentes, ensuciarlos y omitir zone.js y DI.

Ahora tenemos disponibles dos funciones adicionales, exportadas desde @angular/core . Estos son ɵdetectChanges y ɵmarkDirty . Estas dos funciones todavía están destinadas al uso interno y son inestables: el símbolo ɵ se encuentra al comienzo de sus nombres.

Veamos cómo usar estas funciones.

▍ ɵmarkFunción sucia


Esta función le permite marcar un componente, haciéndolo "sucio", es decir, necesita volver a renderizarse. Ella, si el componente no estaba marcado como "sucio" antes de ser llamado, planea comenzar el proceso de detección de cambios.

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

▍ ɵdetectChanges Función


La documentación interna angular dice que, por razones de rendimiento, no debe usar ɵdetectChanges . En cambio, se recomienda usar la función ɵmarkDirty . La función ɵdetectChanges invoca sincrónicamente el proceso de detección de cambios en un componente y sus subcomponentes.

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

Detectar cambios automáticamente usando el decorador TypeScript


Aunque las funciones proporcionadas por Angular aumentan la usabilidad del desarrollo al permitir que el DI funcione, el programador aún puede sentirse frustrado por el hecho de que necesita importar y llamar a estas funciones por sí solo para comenzar el proceso de detección de cambios.

Para simplificar el inicio automático de la detección de cambios, puede escribir un decorador TypeScript, que resolverá este problema de forma independiente. Por supuesto, hay algunas limitaciones aquí, que discutiremos a continuación, pero en mi caso este enfoque resultó ser exactamente lo que necesitaba.

▍Introducción del decorador @observado


Para detectar cambios, haciendo el menor esfuerzo posible, crearemos un decorador que se pueda aplicar de tres maneras. A saber, es aplicable a las siguientes entidades:

  • A los métodos sincrónicos.
  • Objetos observables
  • A los objetos ordinarios.

Considere un par de pequeños ejemplos. En el siguiente fragmento de código, aplicamos el decorador @observed al objeto de state y al método changeTitle :

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

  • Para verificar los cambios en el objeto de state , utilizamos un objeto proxy que intercepta los cambios en el objeto y llama al procedimiento para detectar cambios.
  • changeTitle método changeTitle aplicando una función que primero llama a este método y luego inicia el proceso de detección de cambios.

Y aquí hay un ejemplo con BehaviorSubject :

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

En el caso de los objetos Observables, usar un decorador parece un poco más complicado. Es decir, debe suscribirse al objeto observado y marcar el componente como "sucio" en la suscripción, pero también debe borrar la suscripción. Para hacer esto, reasignamos ngOnInit y ngOnDestroy para suscribirse y limpiarlo más tarde.

▍Creando un decorador


Aquí está la firma decoradora observed :

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

Como puede ver, el descriptor es un parámetro opcional. Esto se debe a que necesitamos que el decorador se aplique tanto a los métodos como a las propiedades. Si el parámetro existe, esto significa que el decorador se aplica al método. En este caso, hacemos esto:

  • Guarde la propiedad del descriptor. value
  • Redefinimos el método de la siguiente manera: llame a la función original y luego llame a markDirty(this) para comenzar el proceso de detección de cambios. Así es como se ve:

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

A continuación, debe verificar qué tipo de propiedad estamos tratando. Puede ser un objeto Observable o un objeto ordinario. Aquí usaremos otra API angular interna. Creo que no está destinado para su uso en aplicaciones normales (¡lo siento!).

Estamos hablando de la propiedad ɵcmp , que da acceso a las propiedades procesadas por Angular después de que se definen. Podemos usarlos para anular los métodos de los onDestroy onInit y onDestroy .

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

Para marcar una propiedad como una para ser monitoreada, usamos ReflectMetadata y establecemos su valor en true . Como resultado, sabremos que necesitamos observar la propiedad cuando se inicializa el componente:

 Reflect.set(target, propertyKey, true); 

Ahora es el momento de anular el onInit y verificar las propiedades al crear la instancia del componente:

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

Definimos la función checkComponentProperties , que checkComponentProperties las propiedades del componente, filtrándolas de acuerdo con el valor establecido previamente usando 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);  }); }; 

La función checkProperty será responsable de decorar propiedades individuales. Primero, verificamos si la propiedad es un Observable o un objeto regular. Si este es un objeto Observable, nos suscribimos a él y agregamos la suscripción a la lista de suscripciones almacenadas en el componente para sus necesidades internas.

 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 {    //    } }; 

Si la propiedad es un objeto ordinario, la convertiremos en un objeto Proxy y llamaremos a markDirty en su función de handler :

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

Finalmente, debe borrar la suscripción después de destruir el componente:

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

Las posibilidades de este decorador no se pueden llamar integrales. No cubren todos los usos posibles que pueden aparecer en una aplicación grande. Por ejemplo, estas son llamadas a funciones de plantilla que devuelven objetos Observables. Pero estoy trabajando en eso.

A pesar de esto, el decorador anterior es suficiente para mi pequeño proyecto. Encontrará su código completo al final del material.

Análisis de resultados de aceleración de aplicaciones.


Ahora que hemos hablado un poco sobre los mecanismos internos de Ivy y sobre cómo crear un decorador usando estos mecanismos, es hora de probar lo que tenemos en una aplicación real.

Yo, para descubrir el efecto de deshacerme de zone.js en el rendimiento de las aplicaciones angulares, utilicé mi proyecto de afición Cryptofolio .

Apliqué el decorador a todos los enlaces necesarios utilizados en las plantillas y zone.js. deshabilitado Por ejemplo, considere el siguiente componente:

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

La plantilla usa dos variables: price (el precio del activo estará aquí) y trend (esta variable puede tomar valores up , stale y down , indicando la dirección del cambio de precio). Los @observed con @observed .

▍ Tamaño del paquete del proyecto


Para comenzar, echemos un vistazo a cuánto ha disminuido el tamaño del paquete del proyecto al deshacerse de zone.js. A continuación se muestra el resultado de construir el proyecto con zone.js.


Resultado de construir un proyecto con zone.js

Y aquí está la asamblea sin zone.js.


El resultado de construir un proyecto sin zone.js

Presta atención al polyfills-es2015.xxx.js . Si el proyecto usa zone.js, su tamaño es de aproximadamente 35 Kb. Pero sin zone.js, solo 130 bytes.

▍ Arranque


Investigué dos opciones de aplicación usando Lighthouse. Los resultados de este estudio se dan a continuación. Cabe señalar que no los tomaría demasiado en serio. El hecho es que al tratar de encontrar los valores promedio, obtuve resultados significativamente diferentes al realizar varias mediciones para la misma versión de la aplicación.

Quizás la diferencia en la evaluación de las dos opciones de aplicación depende solo del tamaño de los paquetes.

Entonces, aquí está el resultado obtenido para una aplicación que utiliza zone.js.


Resultados de análisis para una aplicación que usa zone.js

Y esto es lo que sucedió después de analizar la aplicación en la que zone.js no se usa.


Resultados de análisis para una aplicación que no utiliza zone.js

▍ Rendimiento


Y ahora llegamos a lo más interesante. Este es el rendimiento de una aplicación que se ejecuta bajo carga. Queremos saber cómo se siente el procesador cuando la aplicación muestra actualizaciones de precios de cientos de activos varias veces por segundo.

Para cargar la aplicación, creé 100 entidades que proporcionan datos condicionales a precios que cambian cada 250 ms. Si el precio aumenta, se muestra en verde. Si se reduce - rojo. Todo esto podría cargar seriamente mi MacBook Pro.

Cabe señalar que, mientras trabajaba en el sector financiero en varias aplicaciones diseñadas para la transmisión de alta frecuencia de fragmentos de datos, me he encontrado con una situación similar muchas veces.

Para analizar cómo las diferentes versiones de la aplicación usan los recursos del procesador, utilicé las herramientas de desarrollador de Chrome.

Así es como se ve la aplicación que usa zone.js.


Carga del sistema creada por una aplicación que usa zone.js

Y así es como funciona una aplicación en la que zone.js no se usa.


Carga del sistema creada por una aplicación que no utiliza zone.js

Analizamos estos resultados, prestando atención al gráfico de carga del procesador (amarillo):

  • Como puede ver, una aplicación que utiliza zone.js carga constantemente el procesador en un 70-100%. Si mantiene la pestaña del navegador abierta durante mucho tiempo, creando tal carga en el sistema, entonces la aplicación que se ejecuta puede fallar.
  • Y la versión de la aplicación donde no se utiliza zone.js crea una carga estable en el procesador en el rango de 30 a 40%. Genial

Tenga en cuenta que estos resultados se obtuvieron con la ventana de Chrome Developer Tools abierta, lo que también ejerce presión sobre el sistema y ralentiza la aplicación.

▍ aumento de carga


Traté de asegurarme de que cada entidad responsable de actualizar el precio emitiera 4 actualizaciones más por segundo, además de lo que ya produce.

Esto es lo que logramos averiguar sobre la aplicación en la que zone.js no se usa:

  • Esta aplicación normalmente hizo frente a la carga, ahora utiliza aproximadamente el 50% de los recursos del procesador.
  • Logró cargar el procesador tanto como la aplicación con zone.js, solo cuando los precios se actualizaban cada 10 ms (los datos nuevos, como antes, provenían de 100 entidades).

▍ Análisis de rendimiento con Benchpress angular


El análisis de rendimiento que realicé anteriormente no se puede llamar particularmente científico. Para un estudio más serio del rendimiento de varios marcos, recomendaría usar este punto de referencia . Para la investigación, Angular debe elegir la versión habitual de este marco y su versión sin zone.js.

Yo, inspirado por algunas ideas de este punto de referencia, creé un proyecto que realiza cálculos pesados. Probé su rendimiento con Angular Benchpress .

Aquí está el código del componente probado:

 @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()      });    }  } } 

Lancé un pequeño conjunto de puntos de referencia utilizando Protractor y Benchpress. Las operaciones se realizaron un número específico de veces.


Benchpress en acción

Resultados


Aquí hay una muestra de los resultados obtenidos con Benchpress.


Resultados de Benchpress

Aquí hay una explicación de los indicadores presentados en esta tabla:

  • gcAmount : volumen de operaciones gc (recolección de basura), Kb.
  • gcTime : tiempo de operación de gc, ms.
  • majorGcTime : tiempo de las operaciones principales gc, ms.
  • pureScriptTime : tiempo de ejecución del script en ms, excluyendo las operaciones y la representación de gc.
  • renderTime : tiempo de representación, ms.
  • scriptTime : tiempo de ejecución del script teniendo en cuenta las operaciones y la representación de gc.

Ahora consideraremos el análisis del rendimiento de algunas operaciones en diversas variantes de aplicación. El verde muestra los resultados de una aplicación que usa zone.js, el naranja muestra los resultados de una aplicación sin zone.js. Tenga en cuenta que aquí solo se analiza el tiempo de representación. Si está interesado en todos los resultados de la prueba, marque aquí .

Prueba: crear 1000 líneas


En la primera prueba, se crean 1000 líneas.


Resultados de la prueba

Prueba: crear 10,000 filas


A medida que aumenta la carga en las aplicaciones, también lo hace la diferencia en su rendimiento.


Resultados de la prueba

Prueba: une 1000 líneas


En esta prueba, se agregan 1000 líneas a 10,000 líneas.


Resultados de la prueba

Prueba: eliminar 10,000 filas


Aquí, se crean 10,000 líneas, que luego se eliminan.


Resultados de la prueba

Código fuente del decorador TypeScript


A continuación se muestra el código fuente del decorador TypeScript discutido aquí. Este código también se puede encontrar aquí .

 // 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);    }  }; } 

Resumen


Aunque espero que te haya gustado mi historia sobre la optimización del rendimiento de los proyectos de Angular, también espero no haberte incitado a apresurarte a eliminar zone.js de tu proyecto. La estrategia descrita aquí debería ser el último recurso al que puede recurrir para aumentar el rendimiento de su aplicación Angular.

Primero debe probar enfoques como el uso de la estrategia de detección de cambios de OnPush, la aplicación de trackBy , la desactivación de componentes, la ejecución de código fuera de zone.js, la lista negra de eventos zone.js (esta lista de métodos de optimización puede continuar). El enfoque que se muestra aquí es bastante costoso, y no estoy seguro de que todos estén dispuestos a pagar un precio tan alto por el rendimiento.

De hecho, el desarrollo sin zone.js puede no ser lo más atractivo. Quizás esto no sea solo para la persona que está involucrada en el proyecto, que está bajo su control total. Es decir, es el propietario de las dependencias y tiene la capacidad y el tiempo para llevar todo a su forma adecuada.

Si resultó que probaste todo y crees que el cuello de botella de tu proyecto es precisamente zone.js, entonces quizás deberías intentar acelerar Angular detectando los cambios de forma independiente.

Espero que este artículo le haya permitido ver qué espera Angular en el futuro, de lo que Ivy es capaz y qué zone.js puede hacer para maximizar la velocidad de la aplicación.

Estimados lectores! ¿Cómo optimizas tus proyectos angulares que necesitan el máximo rendimiento?


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


All Articles