Reduce el tamaño del frontend

Cómo usar webpack para que tu app sea lo más pequeña posible

Una de las primeras cosas que debes hacer cuando optimizas una aplicación es hacerla lo más pequeña posible. A continuación, te indicamos cómo hacerlo con webpack.

Webpack 4 introdujo la nueva marca mode. Puedes establecer esta marca en 'development' o 'production' para indicarle a Webpack que estás compilando la aplicación para un entorno específico:

// webpack.config.js
module.exports = {
  mode: 'production',
};

Asegúrate de habilitar el modo production cuando compiles tu app para producción. Esto hará que webpack aplique optimizaciones, como la reducción, la eliminación del código exclusivo para desarrollo en bibliotecas, y mucho más.

Lecturas adicionales

Habilitar reducción

La reducción se produce cuando comprimes el código quitando espacios adicionales, acortando los nombres de variables y así sucesivamente. Para ello, puedes escribir lo siguiente:

// Original code
function map(array, iteratee) {
  let index = -1;
  const length = array == null ? 0 : array.length;
  const result = new Array(length);

  while (++index < length) {
    result[index] = iteratee(array[index], index, array);
  }
  return result;
}

// Minified code
function map(n,r){let t=-1;for(const a=null==n?0:n.length,l=Array(a);++t<a;)l[t]=r(n[t],t,n);return l}

Webpack admite dos formas de reducir el código: la reducción a nivel del paquete y las opciones específicas del cargador. Se deben usar simultáneamente.

Reducción a nivel del paquete

La reducción a nivel del paquete comprime todo el paquete después de la compilación. Funciona de la siguiente manera:

  1. Debes escribir el código de la siguiente manera:

    // comments.js
    import './comments.css';
    export function render(data, target) {
      console.log('Rendered!');
    }
    
  2. Webpack lo compila en aproximadamente lo siguiente:

    // bundle.js (part of)
    "use strict";
    Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
    /* harmony export (immutable) */ __webpack_exports__["render"] = render;
    /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__comments_css__ = __webpack_require__(1);
    /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__comments_css_js___default =
    __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0__comments_css__);
    
    function render(data, target) {
    console.log('Rendered!');
    }
    
  3. Un minificador la comprime en aproximadamente lo siguiente:

    // minified bundle.js (part of)
    "use strict";function t(e,n){console.log("Rendered!")}
    Object.defineProperty(n,"__esModule",{value:!0}),n.render=t;var o=r(1);r.n(o)
    

En webpack 4, la reducción a nivel del paquete se habilita automáticamente, tanto en el modo de producción como sin uno. Usa el minificador UglifyJS de forma interna. (Si alguna vez necesitas inhabilitar la reducción, usa el modo de desarrollo o pasa false a la opción optimization.minimize).

En webpack 3, debes usar el complemento UglifyJS directamente. El complemento viene incluido con webpack. Para habilitarlo, agrégalo a la sección plugins de la configuración:

// webpack.config.js
const webpack = require('webpack');

module.exports = {
  plugins: [
    new webpack.optimize.UglifyJsPlugin(),
  ],
};

Opciones específicas del cargador

La segunda forma de reducir el código es con opciones específicas del cargador (qué es un cargador). Con las opciones del cargador, puedes comprimir los elementos que el minificador no puede reducir. Por ejemplo, cuando importas un archivo CSS con css-loader, el archivo se compila en una cadena:

/* comments.css */
.comment {
  color: black;
}
// minified bundle.js (part of)
exports=module.exports=__webpack_require__(1)(),
exports.push([module.i,".comment {\r\n  color: black;\r\n}",""]);

El minificador no puede comprimir este código porque es una cadena. Para reducir el contenido del archivo, debemos configurar el cargador para que haga lo siguiente:

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          { loader: 'css-loader', options: { minimize: true } },
        ],
      },
    ],
  },
};

Lecturas adicionales

Especifica NODE_ENV=production

Otra forma de disminuir el tamaño del frontend es establecer la variable de entorno NODE_ENV en tu código con el valor production.

Las bibliotecas leen la variable NODE_ENV para detectar en qué modo deberían funcionar, ya sea durante el desarrollo o la producción. Algunas bibliotecas se comportan de forma diferente según esta variable. Por ejemplo, cuando NODE_ENV no está configurado en production, Vue.js realiza verificaciones adicionales y muestra advertencias:

// vue/dist/vue.runtime.esm.js
// …
if (process.env.NODE_ENV !== 'production') {
  warn('props must be strings when using array syntax.');
}
// …

React funciona de manera similar: carga una compilación de desarrollo que incluye las advertencias:

// react/index.js
if (process.env.NODE_ENV === 'production') {
  module.exports = require('./cjs/react.production.min.js');
} else {
  module.exports = require('./cjs/react.development.js');
}

// react/cjs/react.development.js
// …
warning$3(
    componentClass.getDefaultProps.isReactClassApproved,
    'getDefaultProps is only used on classic React.createClass ' +
    'definitions. Use a static property named `defaultProps` instead.'
);
// …

Por lo general, esas verificaciones y advertencias no son necesarias en producción, pero permanecen en el código y aumentan el tamaño de la biblioteca. En webpack 4, quítalos agregando la opción optimization.nodeEnv: 'production':

// webpack.config.js (for webpack 4)
module.exports = {
  optimization: {
    nodeEnv: 'production',
    minimize: true,
  },
};

En el paquete web 3, usa DefinePlugin en su lugar:

// webpack.config.js (for webpack 3)
const webpack = require('webpack');

module.exports = {
  plugins: [
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': '"production"'
    }),
    new webpack.optimize.UglifyJsPlugin()
  ]
};

Tanto la opción optimization.nodeEnv como el DefinePlugin funcionan de la misma manera: reemplazan todos los casos de process.env.NODE_ENV por el valor especificado. Con la configuración anterior, sucede lo siguiente:

  1. Webpack reemplazará todos los casos de process.env.NODE_ENV por "production":

    // vue/dist/vue.runtime.esm.js
    if (typeof val === 'string') {
      name = camelize(val);
      res[name] = { type: null };
    } else if (process.env.NODE_ENV !== 'production') {
      warn('props must be strings when using array syntax.');
    }
    

    // vue/dist/vue.runtime.esm.js
    if (typeof val === 'string') {
      name = camelize(val);
      res[name] = { type: null };
    } else if ("production" !== 'production') {
      warn('props must be strings when using array syntax.');
    }
    
  2. Luego, el minificador quitará todas esas ramas if, ya que "production" !== 'production' siempre es falso y el complemento comprende que el código dentro de estas ramas nunca se ejecutará:

    // vue/dist/vue.runtime.esm.js
    if (typeof val === 'string') {
      name = camelize(val);
      res[name] = { type: null };
    } else if ("production" !== 'production') {
      warn('props must be strings when using array syntax.');
    }
    

    // vue/dist/vue.runtime.esm.js (without minification)
    if (typeof val === 'string') {
      name = camelize(val);
      res[name] = { type: null };
    }
    

Lecturas adicionales

Cómo usar módulos de ES

La siguiente forma de disminuir el tamaño del frontend es usar módulos de ES.

Cuando usas módulos ES, webpack puede realizar el proceso de eliminación de árboles. La eliminación de código no utilizado ocurre cuando un agrupador recorre el árbol de dependencias completo, verifica qué dependencias se usan y quita las que no se usan. Por lo tanto, si utilizas la sintaxis del módulo ES, webpack puede eliminar el código que no se usa:

  1. Escribes un archivo con varias exportaciones, pero la app usa solo una de ellas:

    // comments.js
    export const render = () => { return 'Rendered!'; };
    export const commentRestEndpoint = '/rest/comments';
    
    // index.js
    import { render } from './comments.js';
    render();
    
  2. Webpack comprende que no se usa commentRestEndpoint y no genera un punto de exportación independiente en el paquete:

    // bundle.js (part that corresponds to comments.js)
    (function(module, __webpack_exports__, __webpack_require__) {
    "use strict";
    const render = () => { return 'Rendered!'; };
    /* harmony export (immutable) */ __webpack_exports__["a"] = render;
    
    const commentRestEndpoint = '/rest/comments';
    /* unused harmony export commentRestEndpoint */
    })
    
  3. La reducción quita la variable sin usar:

    // bundle.js (part that corresponds to comments.js)
    (function(n,e){"use strict";var r=function(){return"Rendered!"};e.b=r})
    

Esto funciona incluso con las bibliotecas si están escritas con módulos de ES.

Sin embargo, no es necesario que uses exactamente el minificador integrado de webpack (UglifyJsPlugin). Cualquier minificador que admita la eliminación de código muerto (p.ej., el complemento Babel Minify o el complemento de Google Closure Compiler) hará el truco.

Lecturas adicionales

Optimiza imágenes

Las imágenes representan más de la mitad del tamaño de la página. Si bien no son tan importantes como JavaScript (p. ej., no bloquean la renderización), aún consumen una gran parte del ancho de banda. Usa url-loader, svg-url-loader y image-webpack-loader para optimizarlas en webpack.

url-loader intercala pequeños archivos estáticos en la app. Sin configuración, toma un archivo pasado, lo coloca junto al paquete compilado y muestra una URL de ese archivo. Sin embargo, si especificamos la opción limit, codificará los archivos inferiores a este límite como una URL de datos Base64 y mostrará esta URL. Esto intercala la imagen en el código JavaScript y guarda una solicitud HTTP:

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.(jpe?g|png|gif)$/,
        loader: 'url-loader',
        options: {
          // Inline files smaller than 10 kB (10240 bytes)
          limit: 10 * 1024,
        },
      },
    ],
  }
};
// index.js
import imageUrl from './image.png';
// → If image.png is smaller than 10 kB, `imageUrl` will include
// the encoded image: 'data:image/png;base64,iVBORw0KGg…'
// → If image.png is larger than 10 kB, the loader will create a new file,
// and `imageUrl` will include its url: `/2fcd56a1920be.png`

svg-url-loader funciona como url-loader, excepto que codifica archivos con codificación URL en lugar de Base64. Esto es útil para imágenes SVG. Como los archivos SVG son solo texto sin formato, esta codificación tiene un tamaño más eficaz.

module.exports = {
  module: {
    rules: [
      {
        test: /\.svg$/,
        loader: "svg-url-loader",
        options: {
          limit: 10 * 1024,
          noquotes: true
        }
      }
    ]
  }
};

image-webpack-loader comprime las imágenes que pasan por él. Admite imágenes JPG, PNG, GIF y SVG, así que lo usaremos para todos estos tipos.

Este cargador no incorpora imágenes en la app, por lo que debe funcionar en conjunto con url-loader y svg-url-loader. A fin de evitar copiarlo y pegarlo en ambas reglas (una para imágenes JPG, PNG o GIF y otra para imágenes en formato SVG), incluiremos este cargador como una regla independiente con enforce: 'pre':

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.(jpe?g|png|gif|svg)$/,
        loader: 'image-webpack-loader',
        // This will apply the loader before the other ones
        enforce: 'pre'
      }
    ]
  }
};

La configuración predeterminada del cargador ya está lista, pero si quieres configurarla aún más, consulta las opciones del complemento. Para elegir las opciones que quieres especificar, consulta la excelente guía sobre optimización de imágenes de Addy Osmani.

Lecturas adicionales

Optimiza las dependencias

Más de la mitad del tamaño promedio de JavaScript proviene de dependencias, y una parte de ese tamaño puede ser simplemente innecesario.

Por ejemplo, Lodash (a partir de la versión 4.17.4) agrega 72 KB de código reducido al paquete. Sin embargo, si solo usas, por ejemplo, 20 de sus métodos, aproximadamente 65 KB de código reducido no hacen nada.

Otro ejemplo es Moment.js. Su versión 2.19.1 ocupa 223 KB de código reducido, lo que es enorme. El tamaño promedio de JavaScript en una página fue de 452 KB en octubre de 2017. Sin embargo, 170 KB de ese tamaño corresponden a archivos de localización. Si no usas Moment.js con varios idiomas, estos archivos aumentarán el tamaño del paquete sin ningún propósito.

Todas estas dependencias se pueden optimizar con facilidad. Recopilamos enfoques de optimización en un repositorio de GitHub. Descúbrelo.

Habilitar la concatenación de módulos para módulos ES (también conocida como elevación de alcance)

Cuando compilas un paquete, webpack une cada módulo en una función:

// index.js
import {render} from './comments.js';
render();

// comments.js
export function render(data, target) {
  console.log('Rendered!');
}

// bundle.js (part  of)
/* 0 */
(function(module, __webpack_exports__, __webpack_require__) {
  "use strict";
  Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
  var __WEBPACK_IMPORTED_MODULE_0__comments_js__ = __webpack_require__(1);
  Object(__WEBPACK_IMPORTED_MODULE_0__comments_js__["a" /* render */])();
}),
/* 1 */
(function(module, __webpack_exports__, __webpack_require__) {
  "use strict";
  __webpack_exports__["a"] = render;
  function render(data, target) {
    console.log('Rendered!');
  }
})

En el pasado, esto era necesario para aislar los módulos de CommonJS/AMD entre sí. Sin embargo, esto agregó una sobrecarga de tamaño y rendimiento para cada módulo.

Webpack 2 introdujo la compatibilidad con los módulos ES que, a diferencia de los módulos CommonJS y AMD, se pueden agrupar sin unir cada uno con una función. Además, webpack 3 hizo posible esa agrupación, con la concatenación de módulos. Esto es lo que hace la concatenación de módulos:

// index.js
import {render} from './comments.js';
render();

// comments.js
export function render(data, target) {
  console.log('Rendered!');
}

// Unlike the previous snippet, this bundle has only one module
// which includes the code from both files

// bundle.js (part of; compiled with ModuleConcatenationPlugin)
/* 0 */
(function(module, __webpack_exports__, __webpack_require__) {
  "use strict";
  Object.defineProperty(__webpack_exports__, "__esModule", { value: true });

  // CONCATENATED MODULE: ./comments.js
    function render(data, target) {
    console.log('Rendered!');
  }

  // CONCATENATED MODULE: ./index.js
  render();
})

¿Ves la diferencia? En el paquete simple, el módulo 0 requería render del módulo 1. Con la concatenación de módulos, require simplemente se reemplaza por la función requerida y se quita el módulo 1. El paquete tiene menos módulos y menos sobrecarga.

Para activar este comportamiento, en Webpack 4, habilita la opción optimization.concatenateModules:

// webpack.config.js (for webpack 4)
module.exports = {
  optimization: {
    concatenateModules: true
  }
};

En webpack 3, usa ModuleConcatenationPlugin:

// webpack.config.js (for webpack 3)
const webpack = require('webpack');

module.exports = {
  plugins: [
    new webpack.optimize.ModuleConcatenationPlugin()
  ]
};

Lecturas adicionales

Usa externals si tienes código webpack y otro que no lo es.

Es posible que tengas un proyecto grande en el que parte del código se compila con webpack y otra parte no. Como en un sitio de hosting de videos, en el que el widget del reproductor se compila con Webpack y la página circundante puede no tener las siguientes características:

Captura de pantalla de un sitio de hosting de videos
(Un sitio de hosting de videos completamente aleatorio)

Si ambos fragmentos de código tienen dependencias en común, puedes compartirlos para evitar descargar su código varias veces. Para ello, se usa la opción externals del paquete web, ya que reemplaza los módulos por variables o, también, otras importaciones externas.

Si las dependencias están disponibles en window

Si el código que no es webpack se basa en dependencias que están disponibles como variables en window, usa alias para nombrar dependencias de variables:

// webpack.config.js
module.exports = {
  externals: {
    'react': 'React',
    'react-dom': 'ReactDOM'
  }
};

Con esta configuración, webpack no agrupará los paquetes react ni react-dom. En su lugar, se reemplazarán por algo como lo siguiente:

// bundle.js (part of)
(function(module, exports) {
  // A module that exports `window.React`. Without `externals`,
  // this module would include the whole React bundle
  module.exports = React;
}),
(function(module, exports) {
  // A module that exports `window.ReactDOM`. Without `externals`,
  // this module would include the whole ReactDOM bundle
  module.exports = ReactDOM;
})

Si las dependencias se cargan como paquetes de AMD

Si el código que no es webpack no expone las dependencias en window, el proceso se vuelve más complejo. Sin embargo, puedes evitar cargar el mismo código dos veces si el código que no es de Webpack consume estas dependencias como paquetes AMD.

Para ello, compila el código del paquete web como un paquete AMD y módulos de alias en las URLs de la biblioteca:

// webpack.config.js
module.exports = {
  output: {
    libraryTarget: 'amd'
  },
  externals: {
    'react': {
      amd: '/libraries/react.min.js'
    },
    'react-dom': {
      amd: '/libraries/react-dom.min.js'
    }
  }
};

Webpack unirá el paquete en define() y hará que dependa de estas URLs:

// bundle.js (beginning)
define(["/libraries/react.min.js", "/libraries/react-dom.min.js"], function () {  });

Si el código que no es de webpack usa las mismas URLs para cargar sus dependencias, estos archivos se cargarán solo una vez. Las solicitudes adicionales usarán la caché del cargador.

Lecturas adicionales

En resumen

  • Habilita el modo de producción si usas webpack 4
  • Minimiza el código con las opciones de minificador y cargador a nivel de paquete
  • Para quitar el código exclusivo de desarrollo, reemplaza NODE_ENV por production.
  • Usa módulos de ES para habilitar el movimiento de árboles
  • Comprime las imágenes
  • Aplica optimizaciones específicas de la dependencia
  • Habilitar la concatenación de módulos
  • Usa externals si es conveniente para ti