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.

Cómo usar el modo de producción (solo para Webpack 4)

Webpack 4 introdujo la nueva marca mode. Puedes establecer esta marca en 'development' o 'production' para sugerir 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 la reducción

La reducción ocurre cuando comprimes el código mediante la eliminación de espacios adicionales, la reducción de los nombres de variables, etc. 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. Deben usarse 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. Escribes código como el siguiente:

    // comments.js
    import './comments.css';
    export function render(data, target) {
      console.log('Rendered!');
    }
    
  2. Webpack lo compila aproximadamente en 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 lo 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 en el modo de producción y sin ella. 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 con un 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 son las opciones específicas del cargador (qué es un cargador). Con las opciones del cargador, puedes comprimir 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 de esta manera:

// 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: en el de desarrollo o en el de producción. Algunas bibliotecas se comportan de manera diferente según esta variable. Por ejemplo, cuando NODE_ENV no está configurado como production, Vue.js realiza verificaciones adicionales e imprime 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.'
);
// …

Estas comprobaciones y advertencias suelen ser innecesarias en la producción, pero permanecen en el código y aumentan el tamaño de la biblioteca. En webpack 4, para quitarlos, agrega la opción optimization.nodeEnv: 'production':

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

En webpack 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()
  ]
};

La opción optimization.nodeEnv y DefinePlugin funcionan de la misma manera: reemplazan todos los casos de process.env.NODE_ENV con el valor especificado. Con la configuración anterior:

  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 esas 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 utilizas módulos de ES, webpack puede realizar la eliminación de código no utilizado. La eliminación de código no se produce cuando un agrupador recorre todo el árbol de dependencias, verifica qué dependencias se usan y quita las que no se usan. Por lo tanto, si usas 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 commentRestEndpoint no se usa y no genera un punto de exportación separado 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. El minificador quita la variable que no se usa:

    // 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 bibliotecas si se escriben 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 no muerto (p.ej., el complemento Babel Minify o el complemento 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 gran parte del ancho de banda. Usa url-loader, svg-url-loader y image-webpack-loader para optimizarlos en webpack.

url-loader intercala pequeños archivos estáticos en la app. Sin configuración, toma un archivo que se pasó, lo coloca junto al paquete compilado y muestra una URL de ese archivo. Sin embargo, si especificamos la opción limit, codificará los archivos menores que 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 igual que url-loader, excepto que codifica archivos con la codificación URL en lugar de la base64. Esto es útil para imágenes SVG. Debido a que los archivos SVG son solo texto sin formato, esta codificación es 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 atraviesan. Es compatible con imágenes JPG, PNG, GIF y SVG, así que la usaremos para todos estos tipos.

Este cargador no incorpora imágenes en la app, por lo que debe funcionar en sincronización con url-loader y svg-url-loader. Para evitar copiarlo y pegarlo en ambas reglas (una para imágenes JPG/PNG/GIF y otra para SVG), incluiremos este cargador como 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 qué opciones 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 podría ser innecesaria.

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

Otro ejemplo es Moment.js. Su versión 2.19.1 usa 223 KB de código reducido, lo cual es enorme: el tamaño promedio de JavaScript en una página era de 452 KB en octubre de 2017. Sin embargo, 170 KB de ese tamaño son de archivos de localización. Si no usas Moment.js con varios idiomas, estos archivos sobrepasarán el paquete sin un propósito.

Todas estas dependencias se pueden optimizar con facilidad. Recopilamos enfoques de optimización en un repositorio de GitHub. Revísalo.

Habilita la concatenación de módulos para módulos ES (es decir, la 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!');
  }
})

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

Webpack 2 introdujo compatibilidad con 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 posibilitó la creación de paquetes, con 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 de módulos.

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 un código webpack y otro que no es webpack.

Es posible que tengas un proyecto grande en el que una 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 puede compilar con webpack y la página circundante podría no estarlo:

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

Si ambos fragmentos de código tienen dependencias comunes, puedes compartirlas para evitar descargar su código varias veces. Para ello, usa la opción externals de webpack: reemplaza los módulos con variables o con otras importaciones externas.

Si las dependencias están disponibles en window

Si el código que no es de webhook se basa en dependencias que están disponibles como variables en window, asigna un alias a los nombres de dependencias para los nombres de variables:

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

Con esta configuración, webpack no empaquetará los paquetes react ni react-dom. En cambio, 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 de webhook no expone las dependencias en window, el proceso será más complejo. Sin embargo, puedes evitar cargar el mismo código dos veces si el código que no es de webhook consume estas dependencias como paquetes AMD.

Para ello, compila el código del paquete web como un paquete de AMD y asígnales un alias a las URL 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 webhook 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 cargador y minificador a nivel del paquete
  • Para quitar el código exclusivo de desarrollo, reemplaza NODE_ENV por production.
  • Cómo usar módulos de ES para habilitar la eliminación de código no utilizado
  • Comprime las imágenes.
  • Aplica optimizaciones específicas para la dependencia
  • Habilitar la concatenación de módulos
  • Usa externals si esta opción es adecuada para ti.