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.jsA 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:
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.jsY aquí está la asamblea sin zone.js.
El resultado de construir un proyecto sin zone.jsPresta 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.jsY 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.jsY 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.jsAnalizamos 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ónResultados
Aquí hay una muestra de los resultados obtenidos con Benchpress.
Resultados de BenchpressAquí 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 pruebaPrueba: 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 pruebaPrueba: une 1000 líneas
En esta prueba, se agregan 1000 líneas a 10,000 líneas.
Resultados de la pruebaPrueba: eliminar 10,000 filas
Aquí, se crean 10,000 líneas, que luego se eliminan.
Resultados de la pruebaCó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í .
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?
