Réduire la taille de l'interface

Utiliser Webpack pour réduire au maximum la taille de votre application

L'une des premières choses à faire lorsque vous optimisez une application consiste à la réduire le plus possible. Voici comment procéder avec webpack.

Utiliser le mode production (webpack 4 uniquement)

Webpack 4 a introduit le nouvel indicateur mode. Vous pouvez définir cet indicateur sur 'development' ou 'production' pour indiquer au pack Web que vous créez l'application pour un environnement spécifique:

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

Veillez à activer le mode production lorsque vous compilez votre application pour la production. De cette façon, Webpack appliquera des optimisations telles que la minimisation, la suppression du code réservé au développement dans les bibliothèques, entre autres.

Complément d'informations

Activer la minimisation

La minimisation consiste à compresser le code en supprimant les espaces superflus, en raccourcissant les noms des variables, etc. Exemple :

// 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 accepte deux méthodes pour réduire la taille du code: la minification au niveau du bundle et les options spécifiques au chargeur. Ils doivent être utilisés simultanément.

Minimisation au niveau du bundle

La minimisation au niveau du bundle compresse l'ensemble du bundle après la compilation. Voici comment cela fonctionne :

  1. Vous écrivez le code comme ceci:

    // comments.js
    import './comments.css';
    export function render(data, target) {
      console.log('Rendered!');
    }
    
  2. Webpack le compile approximativement de la manière suivante:

    // 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 réducteur la compresse approximativement de la manière suivante:

    // 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)
    

Dans webpack 4, la minimisation au niveau du bundle est activée automatiquement, aussi bien en mode production qu'en mode production. Il utilise le minificateur UglifyJS en arrière-plan. (Si vous devez désactiver la minimisation, utilisez simplement le mode Développement ou transmettez false à l'option optimization.minimize.)

Dans Webpack 3,vous devez utiliser directement le plug-in UglifyJS. Le plug-in est fourni avec webpack. Pour l'activer, ajoutez-le à la section plugins de la configuration:

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

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

Options spécifiques au chargeur

La deuxième façon de réduire la taille du code consiste à utiliser des options spécifiques au chargeur (ce qu'est un chargeur). Avec les options de chargement, vous pouvez compresser les éléments que le minimateur ne peut pas réduire. Par exemple, lorsque vous importez un fichier CSS avec css-loader, le fichier est compilé dans une chaîne:

/* 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}",""]);

L'outil de réduction de taille ne peut pas compresser ce code, car il s'agit d'une chaîne. Pour réduire la taille du contenu du fichier, vous devez configurer le chargeur:

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

Complément d'informations

Spécifier NODE_ENV=production

Une autre façon de réduire la taille de l'interface consiste à définir la variable d'environnement NODE_ENV dans votre code sur la valeur production.

Les bibliothèques lisent la variable NODE_ENV pour détecter le mode de fonctionnement dans lequel elles doivent fonctionner (en développement ou en production). Certaines bibliothèques se comportent différemment en fonction de cette variable. Par exemple, lorsque NODE_ENV n'est pas défini sur production, Vue.js effectue des vérifications et affiche des avertissements supplémentaires:

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

React fonctionne de la même manière. Il charge un build de développement qui inclut les avertissements:

// 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.'
);
// …

Ces vérifications et avertissements sont généralement inutiles en production, mais ils restent dans le code et augmentent la taille de la bibliothèque. Dans le pack Web 4,supprimez-les en ajoutant l'option optimization.nodeEnv: 'production':

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

Dans webpack 3,utilisez plutôt DefinePlugin:

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

L'option optimization.nodeEnv et DefinePlugin fonctionnent de la même manière : elles remplacent toutes les occurrences de process.env.NODE_ENV par la valeur spécifiée. Avec la configuration ci-dessus:

  1. Webpack remplacera toutes les occurrences de process.env.NODE_ENV par "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. Ensuite, l'outil minifier supprimera toutes les branches if de ce type, car "production" !== 'production' est toujours "false" et le plug-in comprend que le code à l'intérieur de ces branches ne s'exécutera jamais:

    // 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 };
    }
    

Complément d'informations

Utiliser des modules ES

L'autre façon de réduire la taille de l'interface consiste à utiliser des modules ES.

Lorsque vous utilisez des modules ES, webpack peut secouer l'arbre. On parle de tree shaking lorsqu'un bundler parcourt l'ensemble de l'arborescence des dépendances, vérifie quelles sont les dépendances utilisées et supprime celles qui ne sont pas utilisées. Ainsi, si vous utilisez la syntaxe du module ES, webpack peut éliminer le code inutilisé:

  1. Vous écrivez un fichier avec plusieurs exportations, mais l'application n'en utilise qu'une seule:

    // comments.js
    export const render = () => { return 'Rendered!'; };
    export const commentRestEndpoint = '/rest/comments';
    
    // index.js
    import { render } from './comments.js';
    render();
    
  2. Webpack comprend que commentRestEndpoint n'est pas utilisé et ne génère pas de point d'exportation distinct dans le bundle:

    // 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. Le paramètre minifier supprime la variable inutilisée:

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

Cela fonctionne même avec les bibliothèques si elles sont écrites avec des modules ES.

Toutefois, vous n'êtes pas obligé d'utiliser précisément le minimateur intégré du pack Webpack (UglifyJsPlugin). N'importe quel minimateur prenant en charge la suppression du code mort (par exemple, le plug-in Babel Minify ou le plug-in Google Closure Compiler) fera l'affaire.

Complément d'informations

Optimiser les images

Les images représentent plus de la moitié de la taille d'une page. Bien qu'ils ne soient pas aussi essentiels que JavaScript (par exemple, ils ne bloquent pas l'affichage), ils consomment tout de même une grande partie de la bande passante. Utilisez url-loader, svg-url-loader et image-webpack-loader pour les optimiser dans le Webpack.

url-loader intègre de petits fichiers statiques dans l'application. Sans configuration, il reçoit un fichier transmis, le place à côté du bundle compilé et renvoie une URL de ce fichier. Toutefois, si nous spécifions l'option limit, elle encodera les fichiers inférieurs à cette limite en tant qu'URL de données Base64 et renverra cette URL. L'image est alors intégrée dans le code JavaScript et enregistre une requête 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 fonctionne comme url-loader, sauf qu'il encode des fichiers avec l'encodage d'URL au lieu du format Base64. Cet encodage est utile pour les images SVG, car il s'agit d'un simple texte brut.

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

image-webpack-loader compresse les images qui la traversent. Il prend en charge les images JPG, PNG, GIF et SVG, nous allons donc l'utiliser pour tous ces types.

Ce chargeur n'intègre pas d'images dans l'application. Il doit donc fonctionner conjointement avec url-loader et svg-url-loader. Pour éviter de le copier-coller dans les deux règles (une pour les images JPG/PNG/GIF et une autre pour les images SVG), nous allons inclure ce chargeur en tant que règle distincte avec 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'
      }
    ]
  }
};

Les paramètres par défaut du chargeur sont déjà prêts. Toutefois, si vous souhaitez configurer davantage le chargeur, consultez les options du plug-in. Pour choisir les options à spécifier, consultez l'excellent guide sur l'optimisation des images d'Addy Osmani.

Complément d'informations

Optimiser les dépendances

Plus de la moitié de la taille moyenne de JavaScript provient des dépendances, et une partie de cette taille peut être tout simplement inutile.

Par exemple, Lodash (à partir de la version 4.17.4) ajoute 72 Ko de code réduit au bundle. Toutefois, si vous n'utilisez que 20 de ses méthodes, environ 65 Ko de code réduit ne font rien.

Autre exemple : Moment.js. Sa version 2.19.1 utilise 223 ko de code réduit, ce qui est énorme : la taille moyenne du code JavaScript sur une page était de 452 ko en octobre 2017. Cependant, 170 Ko de cette taille correspondent à des fichiers de localisation. Si vous n'utilisez pas Moment.js avec plusieurs langages, ces fichiers surchargeront le bundle sans avoir d'objectif.

Toutes ces dépendances peuvent être facilement optimisées. Nous avons rassemblé des approches d'optimisation dans un dépôt GitHub. Consultez-le.

Activer la concaténation des modules ES (hissage de champ d'application)

Lorsque vous créez un lot, webpack encapsule chaque module dans une fonction:

// 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!');
  }
})

Auparavant, cette opération était nécessaire pour isoler les modules CommonJS/AMD les uns des autres. Toutefois, cela a eu un impact sur la taille et les performances de chaque module.

Webpack 2 est compatible avec les modules ES qui, contrairement aux modules CommonJS et AMD, peuvent être groupés sans encapsuler chacun d'eux avec une fonction. Et webpack 3 a rendu ce regroupement possible, grâce à la concaténation de modules. Voici à quoi sert la concaténation de modules:

// 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();
})

Vous voyez la différence ? Dans le bundle simple, le module 0 requérait render à partir du module 1. Avec la concaténation des modules, require est simplement remplacé par la fonction requise et le module 1 est supprimé. Le bundle contient moins de modules, mais la surcharge de modules est moins importante.

Pour activer ce comportement, dans webpack 4, activez l'option optimization.concatenateModules:

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

Dans webpack 3,utilisez ModuleConcatenationPlugin:

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

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

Complément d'informations

Utilisez externals si vous avez à la fois du code webpack et du code non-webpack.

Vous avez peut-être un projet volumineux dans lequel une partie du code est compilée avec webpack, et une autre partie du code ne l'est pas. Comme pour un site d'hébergement de vidéos, où le widget du lecteur peut être créé avec webpack, la page environnante peut ne pas être:

Capture d&#39;écran d&#39;un site d&#39;hébergement de vidéos
(Site d'hébergement de vidéos complètement aléatoire)

Si les deux éléments de code ont des dépendances communes, vous pouvez les partager pour éviter de télécharger leur code plusieurs fois. Pour ce faire, utilisez l'option externals du pack Web, qui remplace les modules par des variables ou d'autres importations externes.

Si des dépendances sont disponibles dans window

Si votre code non-webpack repose sur des dépendances disponibles en tant que variables dans window, alias de noms de dépendances aux noms de variables:

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

Avec cette configuration, webpack ne regroupera pas les packages react et react-dom. À la place, ils seront remplacés par quelque chose comme ceci:

// 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 les dépendances sont chargées en tant que packages AMD

Si votre code autre que Webpack n'expose pas les dépendances dans window, les choses sont plus compliquées. Toutefois, vous pouvez toujours éviter de charger le même code deux fois si le code autre que Webpack utilise ces dépendances en tant que packages AMD.

Pour ce faire, compilez le code webpack en tant que bundle AMD et modules alias dans les URL de la bibliothèque:

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

Webpack encapsule le bundle dans define() et le fait dépendre des URL suivantes:

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

Si du code autre que "webpack" utilise les mêmes URL pour charger ses dépendances, ces fichiers ne seront chargés qu'une seule fois. Les requêtes supplémentaires utiliseront le cache du chargeur.

Complément d'informations

Récapitulatif

  • Activer le mode production si vous utilisez webpack 4
  • Réduisez votre code grâce aux options de minimisation et de chargeur au niveau du bundle
  • Supprimez le code réservé au développement en remplaçant NODE_ENV par production.
  • Utiliser des modules ES pour activer le tree shaking
  • Compresser les images
  • Appliquer des optimisations spécifiques aux dépendances
  • Activer la concaténation des modules
  • Utilisez externals si cette approche est pertinente pour vous.