Alguna vez te has preguntado si una librería tan simple puede afectar al rendimiento, ¿o no puede? O, por qué me obliga mi librería a instalar todas peerDependencies, sabiendo que no voy a utilizar los módulos que son dependientes de librerías de terceros. O, te has preguntado, ¿qué son y por qué necesitamos puntos de entrada secundarios? Entonces, si te has formulado algunas de las anteriores preguntas, este es tu post :)

Dado que una librería se puede utilizar en muchos lugares, el rendimiento es un aspecto crítico. Y, en este punto, ¡entran en juego los puntos de entrada secundarios!

Al aprovechar los puntos de entrada secundarios, buscamos una mejor arquitectura, compatibilidad con treeshaking, tamaños de paquetes más pequeños y, por lo tanto, aplicaciones más rápidas.

Antes de nada, os dejo un glosario de términos.

Nuestro caso

Y ¿qué mejor que entender esto con un ejemplo?

Empezamos creando nuestra librería de componentes usando el CLI de Angular:

$ ng new my-lib --create-application=false

$ cd my-lib
$ ng generate library my-lib

Nuestra librería va a constar de dos módulos: simple-text y custom-time.

$ ng g c simple-text
$ ng g m simple-text

$ ng g c custom-time
$ ng g m custom-time

simple-text.component.ts va a ser un componente sin dependencias.

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

@Component({
  selector: 'simple-text',
  template: `
    <div>I have no dependencies!</div>`
})

export class SimpleTextComponent {}

custom-time.component.ts va a depender de la librería moment. Ejecutamos el siguiente comando para instalar la librería:

$ npm i moment
import { Component } from '@angular/core';
import * as moment_ from 'moment';
const moment = moment_;

@Component({
  selector: 'custom-time',
  template: `
    <div>Hey, Custom Time:</div>
    <div>{{ time }}</div>
  `
})

export class CustomTimeComponent {
  time: string;

  constructor() {
    this.time = moment().format();
  }
}

Exportamos en el fichero public-api.ts los componentes y módulos creados anteriormente.

/*
 * Public API Surface of my-lib
 */
export * from './simple-text/simple-text.component';
export * from './simple-text/simple-text.module';

export * from './custom-time/custom-time.component';
export * from './custom-time/custom-time.module';

Ya que uno de nuestros componentes depende de moment, debemos especificarla en nuestro my-lib/package.json:

{
 "name": "my-lib",
 "version": "0.0.1",
 "peerDependencies": {
   "@angular/common": "^8.2.14",
   "@angular/core": "^8.2.14",
   "moment": "^2.26.0"
 }

Y llegamos al problema

Vamos a crear una aplicación de ejemplo que va a consumir nuestra librería:

$ ng new example-app

Antes de instalar nuestra librería, veremos qué dependencias tiene nuestra aplicación y cuánto pesan de forma gráfica con Webpack Bundle Analyzer.

$ npm i -D webpack-bundle-analyzer

Añadimos el siguiente script al package.json.

"analyze": "ng build --prod --stats-json && webpack-bundle-analyzer ./dist/example-app/stats.json"

Al ejecutar este comando, se realiza una compilación para producción y se genera un stats.json que luego es recogido y visualizado por webpack-bundle-anlyzer.

Librería Angular  2

Nuestro bundle principal incluye sólo Angular. El tamaño de nuestra aplicación actualmente es de 127.06KB.

Librería Angular  3

Ahora vamos a instalar nuestra librería y ver cómo afecta al tamaño del bundle del proyecto.

Ya que estamos trabajando en este ejemplo con una librería en nuestro entorno local, debemos compilarla y copiar el dist/my-lib dentro de el node_modules de nuestra aplicación.

Importamos solo el módulo simple-texten app.component.module.

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { SimpleTextModule } from 'node_modules/my-lib';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    SimpleTextModule,
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Ejecutamos la aplicación y vemos que obtenemos un error en la terminal.

./node_modules/my-lib/fesm2015/my-lib.js:4:0-33 - Error: Module not found: Error: Can't resolve 'moment' in '/home/eogueta/example-app/node_modules/my-lib/fesm2015'

Aunque nuestra aplicación solo importa SimpleTextModule , el compilador de Angular nos pide instalar todas las peerDependencies definidas en my-lib, así que deberemos instalar moment.

$ npm i moment

Vamos a volver a lanzar el script de analyze:

Librería Angular  4
Librería Angular 5

Vemos que aunque no se esté usando directamente la librería moment, está incluida en nuestro bundle y el tamaño ha aumentado considerablemente, 506.74KB.

Esto se debe a que cuando construimos la librería se genera solo un chunk(my-lib.js) que contiene tanto SimpleTextComponent como CustomTimeComponent. Así que todavía obtenemos la librería moment incluso si solo importamos SimpleTextModule.

Vamos a implementar puntos de entrada secundarios

Hay varias razones por las que queremos usar puntos de entrada secundarios al diseñar nuestras librerías de Angular.

Para este ejemplo vamos a establecer custom-time como punto de entrada secundario siguiendo la estructura definida por ng-packagr, mientras que simple-text seguirá igual que hasta ahora.

De acuerdo con la documentación, esta sería la estructura:

my-lib
├── src
|   ├── lib
|   |    └── simple-text 
|   ├── public_api.ts (primary entry point)
|   └── *.ts
├── ng-package.json
├── package.json
└── custom-time(secondary entry point)
    ├── custom-time.component.ts
    ├── …
    ├── index.ts
    ├── public_api.ts
    └── package.json

Para trabajar con puntos de entrada secundarios, se necesita un mínimo de cuatro archivos por feature.

Librería Angular  7
  1. Agregamos index.ts, package.json y public_api.ts.

El index.ts solo está ahí para apuntar a public_api, que es útil durante las importaciones.

export * from './public_api';

Public_api.ts exporta todos los módulos y componentes de nuestro módulo.

/
 * Public API Surface of custom-time
 */
export * from './custom-time.component';
export * from './custom-time.module';

El package.json contiene configuraciones específicas de ng-packagr.

{
  "ngPackage": {
    "lib": {
      "entryFile": "public_api.ts"
    }
  },
  "peerDependencies": {
   // Lista de las peerDependecies de cada módulo
   "moment": "^2.29.1"
  }
}
  1. Eliminamos la configuración que hemos especificado previamente en el archivo projects/my-lib/package.json, ya que la acabamos de definir en el nuevo fichero projects/my-lib/custom-time/package.json.
  2. Borramos las exportaciones de custom-time de nuestro archivo principal public_apit.ts.
/
 * Public API Surface of my-lib
 */
export * from "./lib/simple-text/simple-text.component";
export * from "./lib/simple-text/simple-text.module";

  1. Construimos la librería.

Terminamos con 2 chunks: my-lib.js, my-lib-custom-time.js.

Librería Angular  8

Cada una de nuestros componentes/features obtiene su propio archivo .js, lo que significa que están incluidas en el paquete de su aplicación individualmente, favoreciendo la técnica de tree-shaking.

my-lib-custom-time.js ahora solo contiene el código relacionado con CustomTimeModule, y my-lib.js el código específico de SimpleTextModule.

Librería Angular  9

Ejecutamos por última vez npm i analyze y podemos ver que el tamaño del bundle se ha reducido, 127.19KB.

Librería Angular  10
Librería Angular 11
  1. Modificamos las rutas a la hora de importar.

Hay un ligero cambio a la hora de importar las rutas, ya que hemos movido nuestro custom-time del punto de entrada principal.

// Primary entry points
import { SimpleText } from 'my-lib';

// Secondary entry points
import { CustomTimeModule } from 'my-lib/custom-time';

Si la aplicación cliente solo importa el componente SimpleTextModule, deberíamos poder ejecutar la aplicación sin instalar moment.

Conclusión

Como hemos dicho antes, los puntos de entrada secundarios nos ofrecen una excelente manera de entregar nuestra librería en múltiples chunks.

Esto es lo que utiliza el equipo de Angular para las bibliotecas @angular/core y @angular/material. La mayoría de nosotros solamente usamos un pequeño subconjunto de los componentes de @angular/material y, al importar únicamente los módulos necesarios, el estilo o la lógica de otros componentes no necesitan incluirse innecesariamente en el paquete final de la aplicación.

Podemos sacar las siguientes conclusiones:

Cosas a tener en cuenta:

Este artículo está basado en:

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete

Estamos comprometidos.

Tecnología, personas e impacto positivo.