Webpack: poniendo a dieta tus bundles (II)


Intro

En la entrega anterior vimos cómo podíamos usar webpack-bundle-analyzer para ver que librerías de terceros hacían que nuestro "bundle" estuviera gordo.

En esta entrega vamos a ver cómo habilitar tree shaking en esas librerías y reducir su tamaño de forma considerable. Como ejemplo trabajaremos con una librería de componentes popular: material-ui, crearemos un aplicación simple y veremos cómo reducir el espacio que ocupa esta librería en nuestra aplicación, pasaremos de un peso de 960 KB a 56 KB.

El código de estas demos lo podéis encontrar en: https://github.com/Lemoncode/treeshaking-samples

En este post trabajaremos con ejemplos desarrollados en TypeScript, pero lo mismo aplicaría para ES6 (en breve tendréis versión de ES6 de los ejemplos, estamos trabajando en ello).

Sin tree shaking

Vamos a empezar por utilizar la librería tal cual. En esta ruta podéis encontrar el fuente de este ejemplo así como un readme.md que incluye una guía paso a paso.


El fuente de esta demo (que incluye una guía paso a paso para reproducirlo) lo podéis encontrar en el siguiente enlace: https://github.com/Lemoncode/treeshaking-samples/tree/master/typescript/01%20no-treeshaking-material-ui


Vamos a importar los elementos de material-ui de forma normal (también podríamos haber usado import * as) :

import {AppBar} from "material-ui";
import {MuiThemeProvider} from "material-ui/styles";

Si ejecutamos un npm run build veremos que nuestro bundle toma un tamaño considerable por culpa de material-ui.

npm run build

Mapa:

notreeshaking.png

Al ver esto no nos queda otra que gritar ¡Houston tenemos un problema!

[Solución recomendada] Cambiando la ruta de los imports

Vamos a hacer una prueba muy tonta: la versión de material-ui que nos bajamos (la última estable), nos permite importar directamente por nombre de componente, si en el fichero de nuestra aplicación (app.tsx) cambiamos las rutas que teníamos por unas exactas al fichero que contiene el componente tenemos que:


El fuente de esta demo (que incluye una guía paso a paso para reproducirlo) lo podéis encontrar en el siguiente enlace: https://github.com/Lemoncode/treeshaking-samples/tree/master/typescript/02%20treeshaking-material-ui


 

Originalmente en el app.tsx teníamos:

import {AppBar} from "material-ui";
import {MuiThemeProvider} from "material-ui/styles";

Ahora pasamos a tener:

import AppBar from 'material-ui/AppBar';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';

La maquinaria de webpack se pone en marcha y debido a que ahora estamos apuntando directamente a los ficheros de los componentes que usamos, a pesar de estar transpilados a ES5, no estamos trayéndonos toda la librería a la hora de incluirla en el bundle. ¿En cuánto nos quedamos?

treeshaking.png

No esta nada mal, hemos bajado de 985 KB a 160.5 KB. Esto nos deja un sabor agridulce, por un lado ¿Se podría llegar a bajar más peso si usamos modulos de ES6? y por otro, me gusta la sintaxis de barrels, o, no me gusta ver 20 líneas de imports sobre una misma librería, ¿No hay forma de seguir usándola y que aplique el tree shaking? A fecha de hoy nuestra recomendación es que utilizes la aproximación de usar nombres de ruta completo y no utilices la sintaxis de barrels, en las siguientes secciones veremos dos soluciones alternivas pero que tienen sus problemas.

Barrels y babel-plugin-import

Una forma de usar los imports con llave es utilizar  este plugin se va a encargar de reemplazar los imports por el de ruta completa en el proceso de transpilación

 


Este plugin no funciona correctamente si en webpack añadimos un bundle para librerias (vendors)

El fuente de esta demo (que incluye una guía paso a paso para reproducirlo) lo podéis encontrar en el siguiente enlace: https://github.com/Lemoncode/treeshaking-samples/tree/master/typescript/03A%20barrels


Instalamos el paquete

npm install babel-plugin-import --save-dev

Añadimos a la configuración de .babelrc la siguiente entrada:

"plugins": [
   ["import", {"libraryName": "material-ui", "libraryDirectory": "", "camel2DashComponentName": false}]
]

 

Podemos mantener nuestros imports con barrels app.tsx

import {AppBar} from "material-ui";
import {MuiThemeProvider} from "material-ui/styles";

Y si ejecutamos

npm run build

Tenemos que material ui sólo ocupa 160 Kb en vez de 980 Kb

barrelsA.png

 

Trabajando con ES6

Aquí va otra opcíon

Vamos a darle una vuelta de tuerca más a ésto, dónde queremos llegar:

  • Trabajar con el código de las librerías en ES6
  • Usar barrels y que se pudiera seguir haciendo tree shaking.

En el caso de material-ui la próxima versión que se va a publicar nos ofrece también su versión en ES6 al hacer install (mira dentro de node_modules/material-ui/es).


Atención: Esta aproximación es un "hack" y desde material-ui no la aconsejan ya que podrían dar problemas en navegadores antiguos.

El fuente de esta demo (que incluye una guía paso a paso para reproducirlo) lo podéis encontrar en el siguiente enlace: https://github.com/Lemoncode/treeshaking-samples/tree/master/typescript/03B%20barrels


Lo pasos que vamos a seguir:

  • Instalar la nueva versión de material-ui
  • Introducir un alias en webpack para que en el caso de material-ui apunte a la versión ES6 de la misma.
  • Volver a usar los imports con barrel (ojo lo imports que realizaremos difieren con el ejemplo anterior ya que la nueva versión de material-ui introduce breaking changes).

Vamos a instalar la última versión de material-ui (al momento de escribir este post estaba en Beta) ya que la última versión estable (al momento de escribir el post, 0.20.0) no exporta sus componentes utilizando módulos ES6. Esto es importante ya que el tree shaking no es compatible con módulos CommonJS (aunque existen plugins para aplicar tree shaking, hay casos que a día de hoy no pueden cubrir), pero sí con módulos ES6.

Cuando importamos con _barrel_ de una librería webpack resuelve internamente cargando todo el contenido del fichero, por lo que inicialmente nuestro bundle tiene toda la librería. Pero  el código que no utilicemos de librerías que usa módulos de ES6 internamente clasifica métodos usados y métodos no usados, por lo que, en caso de webpack, Uglify es el encargado de eliminar el _dead code_ de nuestro bundle, reduciendo así su tamaño.

Entonces instalaremos la última versión de material-ui:

npm install material-ui@next --save

 

Desinstalamos los typings de la anterior ya que la nueva los trae incorporados:

npm uninstall @types/material-ui --save-dev

Introducimos en la configuración de webpack un alias para que apunte a la version ES6

webpack.config.js

  resolve: {
    extensions: ['.js', '.ts', '.tsx'],
    alias: {
      "material-ui": 'material-ui/es'
    }
  },

Aunque la última versión de material-ui trae sus componentes con versión ES6 no hará el tree shaking sin este paso en la configuración webpack. Esto es debido a una solución de compromisa que ha adoptado el equipo de material-ui donde su _index.es.js_ exporta los componentes transpilados en vez de los creados con ES6 localizados en el directorio "material-ui/es" (al momento de escribir hay un issue reportado en el podrás encontrar el razonamiento de porque se ha seguido esta aproximación por parte de los autores de la librería).

Ya podemos sustituir los imports por barrels:

./src/app.js

import {AppBar, Toolbar, Typography, MuiThemeProvider} from "material-ui";

Actualizamos el render del componente app para que se adapte a la nueva versión de material-ui

  public render() {
    return (
      <>
        <AppBar position="static" color="default">
          <Toolbar>
            <Typography variant="title" color="inherit">
              Title
            </Typography>
          </Toolbar>
        </AppBar>
        <HelloComponent userName={this.state.userName} />
        <NameEditComponent userName={this.state.userName} onChange={this.setUsernameState} />
      </>
    );
  }
}

Hacemos un build:

npm run build

 

Si chequeamos el tamaño del bundle, tenemos que:

Sorpresa ¡material-ui sólo pesa 56Kb en nuestro bundle!

barrels.png

 

Conclusión

Que nuestra aplicación pese poco es importante, esto reduce el tiempo de carga y hace que mejore la experiencia de usuario. Con la de librerías de terceros que hay, y que añaden muchos "trucos" nuevos a nuestros desarrollos hay que cuidar mucho cuanto peso van añadiendo, es fácil acabar con un bundle sin minificar que pese del orden de 3 MB a 7 MB.

Aplicando los principios que hemos comentado en esta serie de artículos podemos de forma sencilla bajar el peso de nuestro bundle a menos de la mitad y si nos aplicamos a fondo, llegar a cotas más bajas aún.

 

¿Con ganas de aprender desarrollo Front End?

Si tienes ganas de ponerte al día en el mundo del Front End impartimos un máster online con clases en vivo, más información: http://lemoncode.net/master-frontend