Enmilocalfunciona

Thoughts, stories and ideas.

Ivy, el último gran cambio que se nos viene en Angular

Publicado por Pol Gabarró Mora el

AngularionicIvyFront

Con la llegada de Angular 9 nos encontramos frente a un importante cambio. Se abandona el uso del antiguo motor de renderizado usado hasta ahora, el Renderer2, para pasar al nuevo Ivy.

alt

Esto es la mayor novedad desde la implementación del Renderer2 en Angular 4. Ivy tendrá un gran impacto en el tamaño, rendimiento y desarrollo de nuestras apps. A lo largo del siguiente artículo explicaré cómo funciona el actual motor de renderizado de Angular y qué es lo que propone Ivy.

¿Vale... pero qué es un motor de renderizado?

En AngularJS, el html donde están los componentes y directivas se interpretaba en el navegador. Por ejemplo, si tu usabas un ng-repeat, el framework de Angular 1.x se recorría el DOM entero, leía esta directiva y la interpretaba haciendo un loop.
Todas estas operaciones de leer el DOM y editarlo son lentas en los navegadores actuales, por eso se popularizaron frameworks como React con su virtual-dom donde se trabajaba toda la lógica sobre una copia virtual del DOM (el famoso render), y controlaba que solo se hacían ediciones y modificaciones en el DOM cuando era estrictamente necesario. Esto provocó que AngularJS se empezara a quedar muy atrás en temas de rendimiento y, es entonces, cuando se planteó la arquitectura actual.
En Angular 2+, el html donde está la estructura de la app es simplemente un template que imita el estilo de marcaje html pero que no se puede interpretar en los navegadores. Cuando se hace un ng start el compilador de angular ngc parsea el template y lo convierte en código javascript optimizado. Después, cuando se ejecute la app, este código será interpretado por el Renderer2 de angular y este pintará el DOM.

Cómo funciona el actual Renderer2

Por ejemplo, si tenemos un componente como el siguiente:

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

@Component({
  selector: 'app-root',
  template: `<div>
    <span>Hello {{name}}</span>
  </div>`})
export class AppComponent {  
  name = 'world';
}

Cuando le damos a generar build, el compilador (con AOT) transformará el template HTML en un template data. Este código será el que irá en el bundle de la aplicación. Cuando la app se ejecute este template data será interpretado en tiempo de ejecución por el Angular Interpreter que hará los cambios que sean necesarios en el DOM para el correcto renderizado de la página.

El template data que genera el compilador de Angular es el siguiente:

//1
import * as i0 from "@angular/core";  
import * as i1 from "./app.component";

var styles_AppComponent = [];  
var RenderType_AppComponent = i0.ɵcrt({  
  encapsulation: 2,
  styles: styles_AppComponent,
  data: {}
});
export { RenderType_AppComponent };

//2
export function View_AppComponent_0(_l) {  
  return i0.ɵvid(
    0,
    [
      (_l()(), i0.ɵeld(0, 0, null, null, 2, "div", [], null, null, null, null, null)),
      (_l()(), i0.ɵeld(1, 0, null, null, 1, "span", [], null, null, null, null, null)),
      (_l()(), i0.ɵted(2, null, ["Hello ", ""]))
    ],
    null,
    function(_ck, _v) {
      var _co = _v.component;
      var currVal_0 = _co.name;
      _ck(_v, 2, 0, currVal_0);
    }
  );
}
//3
export function View_AppComponent_Host_0(_l) {  
  return i0.ɵvid(
    0,
    [(_l()(),i0.ɵeld(0,0,null,null,1,"app-root",[],null,null,null,View_AppComponent_0,RenderType_AppComponent)),
      i0.ɵdid(1, 49152, null, 0, i1.AppComponent, [], null, null)
    ],null, null);
}
//4
var AppComponentNgFactory = i0.ɵccf(  
  "app-root",
  i1.AppComponent,
  View_AppComponent_Host_0,
  {},
  {},
  []
);
export { AppComponentNgFactory };  
//# sourceMappingURL=app.component.ngfactory.js.map

La primera parte son los metadatos del componente, como por ejemplo si tiene estilos adjuntos, la encapsulación, etc.

La segunda parte contiene el código con los datos del template que hemos definido nosotros.  Se puede observar cómo se definen las etiquetas 'div' y 'span' y como el binding '{{ }}' es transformado en una función con un callback que "setea" el valor definido por el componente cuando hay un cambio de valor.

La parte 3 contiene la encapsulación del código del template. Y el paso 4 es la función factory que recoge todo lo que hemos definido hasta ahora y que ejecutará el intérprete cuando arranque la app Angular.

Cómo se ve en el código, se genera una estructura de datos bastante complicada que tendrá que ser interpretada por Angular Renderer en tiempo de ejecución.

Ahora vamos al IVY

Si generamos el mismo código con IVY obtenemos lo siguiente:

import { Component } from '@angular/core';  
import * as i0 from "@angular/core";  
export class AppComponent {  
    constructor() {
        this.name = 'world';
    }
}
AppComponent.ngComponentDef = i0.ɵɵdefineComponent({  
  type: AppComponent,
  selectors: [["app-root"]],
  factory: function AppComponent_Factory(t) { return new (t || AppComponent)(); }, consts: 3, vars: 1,
  template: function AppComponent_Template(rf, ctx) {
    if (rf & 1) {
        i0.ɵɵelementStart(0, "div");
        i0.ɵɵelementStart(1, "span");
        i0.ɵɵtext(2);
        i0.ɵɵelementEnd();
        i0.ɵɵelementEnd();
    } if (rf & 2) {
        i0.ɵɵselect(2);
        i0.ɵɵtextInterpolate1("Hello ", ctx.name, "");
    }
}, encapsulation: 2 });

i0.ɵsetClassMetadata(AppComponent, [{  
        type: Component,
        args: [{
                selector: 'app-root',
                template: `<div>
    <span>Hello {{name}}</span>
  </div>`
            }]
    }], null, null);
//# sourceMappingURL=app.component.js.map

Aquí podemos observar una diferencia crucial. Y es que no hay funciones tipo factory. Esto se debe a que los datos del template pasan a hacer llamadas directamente a Angular sin necesidad de pasar por un intérprete.

Con esto ganamos 3 cosas:

  • Nos ahorramos un paso haciendo las apps de Angular mucho más rápidas.
  • Tenemos un código que, al no usar funciones factory y ser simplemente llamadas al core para renderizar hace que sea tree-shackeable, por lo tanto un compilador o optimizador puede eliminar funciones no utilizadas más fácilmente y hacer que el tamaño final del bundle sea menor.
  • Y por último tenemos un código que es más fácil depurarlo y detectar los errores ya que ahora ya no es el intérprete el que da error cuando algo falla en Angular, si no el propio template de nuestra app, los errores nos dirán exactamente el punto del template donde ha habido el error. Por lo tanto tendremos una programación mucho más sencilla.

Esto último se puede ver más claramente si analizamos con profundidad la función encargada del pintado del template:

function AppComponent_Template(rf, ctx) {  
    if (rf & 1) {
        i0.ɵɵelementStart(0, "div");
        i0.ɵɵelementStart(1, "span");
        i0.ɵɵtext(2);
        i0.ɵɵelementEnd();
        i0.ɵɵelementEnd();
    } if (rf & 2) {
        i0.ɵɵselect(2);
        i0.ɵɵtextInterpolate1("Hello ", ctx.name, "");
    }
}

La función encargada de generar el DOM vemos cómo empieza con un if ( rf & 1), esto sirve para indicarle a Angular que esté código solo será ejecutado la primera vez que haga el renderizado.

Después se ve como se hace una llamada al método i0.ɵɵelementStart(0, "div"), donde se le indica que en este punto empieza un elemento div con identificador 0, después se hace otra llamada para indicarle que viene otro elemento span anidado con identificador "1". La tercera llamada es para indicarle que aquí hay un texto, con identificador "2". Las dos siguientes llamadas sirven para cerrar los tags div y span. Con estas instrucciones, el core de Angular renderiza directamente el DOM con la estructura del template.

El siguiente if sirve para indicarle que hay un elemento (2) que cada ciclo de Angular debe renderizarse de nuevo. i0.eeselect(2) para selecionar el elemento y i0.eetextinterpolate para indicarle cómo renderizarlo. O sea interpolar el "Hello" con el contenido de la variable name (world).

Como hemos visto el código se explica solo facilitando mucho la depuración de la app y hasta permitiendo editar el código generado cosa antes completamente imposible.

Vale, pero quiero ver números, ¿realmente se consigue una mejora?

Sí, por ejemplo si compilamos la app de pruebas que hemos definido antes sin Ivy y con Ivy:

Sin Ivy (Angular 8)

Con Ivy (Angular 8)

Como se ve en el bundle generado, se ha conseguido generar el código un 43% más rápido y el peso del main.js es un 10% menor con Iby en Beta. Para futuras versiones se dice que aún deberíamos ver diferencias MUCHO mayores.

Cuando salga Ionic5 de forma oficial desarrollares otro artículo mostrando una batería de pruebas para validar si realmente las promesas de los devs de Angular son ciertas en una aplicación de producción.

Conclusiones

Con Angular 9 (e Ionic 5) Ivy ya vendrá activado por defecto. Este cambio en el motor de renderizado y en la forma en cómo se generan los archivos propiciará que programar en Angular sea más fácil y mejor, apps más rápidas y con un tamaño menor. Y lo mejor es que, si el equipo de Angular cumple las promesas, este cambio tecnológico será totalmente transparente para los developers.

Si te ha gustado, ¡síguenos en Twitter para estar al día de nuevas entregas!