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 est de la rendre aussi petite que possible. Voici comment procéder avec webpack.

Utiliser le mode de production (webpack 4 uniquement)

Webpack 4 a introduit le nouveau paramètre mode. Vous pouvez définir cet indicateur sur 'development' ou 'production' pour indiquer à Webpack que vous compilez 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. Webpack appliquera alors des optimisations telles que la minimisation, la suppression du code réservé au développement dans les bibliothèques, et bien plus encore.

Documentation complémentaire

Activer la minification

La minimisation consiste à compresser le code en supprimant les espaces superflus, en raccourcissant les noms de 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 prend en charge deux méthodes pour réduire la taille du code: la minimisation au niveau du bundle et les options spécifiques au chargeur. Ils doivent être utilisés simultanément.

Minimisation au niveau du bundle

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

  1. Vous écrivez du code comme ceci:

    // comments.js
    import './comments.css';
    export function render(data, target) {
      console.log('Rendered!');
    }
    
  2. Webpack le compile approximativement comme suit:

    // 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 outil de réduction de taille le compresse approximativement dans ce qui suit:

    // 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 minification au niveau du bundle est activée automatiquement, à la fois en mode production et sans mode de production. Il utilise le réducteur 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 méthode consiste à utiliser des options spécifiques au chargeur (qu'est-ce qu'un chargeur). Les options du chargeur vous permettent de compresser des éléments que le réducteur 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 ne peut pas compresser ce code, car il s'agit d'une chaîne. Pour réduire la taille du contenu du fichier, nous devons configurer le chargeur de manière à ce qu'il effectue les opérations suivantes:

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

Documentation complémentaire

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 leur mode de fonctionnement (en développement ou de 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 supplémentaires et affiche des avertissements:

// 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. Elle charge un build de développement qui inclut les avertissements suivants:

// 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 webpack 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 l'option 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. Le minificateur supprimera ensuite toutes ces branches if, car "production" !== 'production' est toujours faux et que le plug-in comprend que le code 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 };
    }
    

Documentation complémentaire

Utiliser des modules ES

La deuxième façon de réduire la taille du front-end consiste à utiliser des modules ES.

Lorsque vous utilisez des modules ES, webpack peut effectuer un tree shaking. On parle de secouement d'arborescence lorsqu'un bundler parcourt l'intégralité de l'arborescence de dépendances, vérifie quelles dépendances sont 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 minificateur 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.

Vous n'êtes pas obligé d'utiliser précisément le minificateur intégré de webpack (UglifyJsPlugin). Tout outil de réduction de taille compatible avec la suppression des codes morts (par exemple, le plug-in Babel Minify ou le plug-in Google Closure Compiler) fera l'affaire.

Documentation complémentaire

Optimiser les images

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

url-loader intègre de petits fichiers statiques dans l'application. Sans configuration, il prend 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 de moins de 2 Mo en tant qu'URL de données Base64 et renverra cette URL. L'image est intégrée au code JavaScript et une requête HTTP est enregistrée :

// 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 exactement comme url-loader, sauf qu'il encode les fichiers avec l'encodage URL au lieu de l'encodage base64. Cette approche est utile pour les images SVG, car les fichiers SVG ne sont qu'un texte brut, cet encodage est plus efficace en termes de taille.

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

image-webpack-loader compresse les images qui y passent. Il est compatible avec 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 en association avec url-loader et svg-url-loader. Pour éviter de copier et coller le code 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, mais si vous souhaitez le configurer davantage, consultez les options du plug-in. Pour choisir les options à spécifier, consultez l'excellent guide d'optimisation des images d'Addy Osmani.

Documentation complémentaire

Optimiser les dépendances

Plus de la moitié de la taille moyenne du code 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 minifié au bundle. Toutefois, si vous n'utilisez que 20 de ses méthodes, environ 65 ko de code minifié ne servent à rien.

Moment.js est un autre exemple. Sa version 2.19.1 occupe 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. Toutefois, 170 Ko correspond à des fichiers de localisation. Si vous n'utilisez pas Moment.js avec plusieurs langues, ces fichiers alourdiront le bundle sans raison.

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

Activer la concaténation des modules ES (c'est-à-dire le hissage de champ d'application)

Lorsque vous créez un bundle, 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, cela était nécessaire pour isoler les modules CommonJS/AMD les uns des autres. Toutefois, cela a ajouté une surcharge de taille et de performances pour chaque module.

Webpack 2 a introduit la prise en charge des modules ES qui, contrairement aux modules CommonJS et AMD, peuvent être regroupés sans être encapsulés chacun avec une fonction. Webpack 3 a rendu ce regroupement possible, avec la concaténation des modules. Voici ce que fait 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 nécessite l'autorisation render 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 comporte moins de modules et moins de frais généraux.

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

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

Documentation complémentaire

Utilisez externals si vous avez à la fois du code webpack et un autre code

Vous pouvez avoir un projet volumineux dans lequel certains codes sont compilés avec webpack et d'autres non. Comme sur un site d'hébergement de vidéos, où le widget du lecteur peut être créé avec webpack, et où 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 vidéo 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 de webpack. Elle remplace les modules par des variables ou d'autres importations externes.

Si les dépendances sont disponibles dans window

Si votre code non webpack repose sur des dépendances disponibles en tant que variables dans window, remplacez les noms de dépendance par des noms de variables :

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

Avec cette configuration, webpack ne regroupe 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 le 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 non-webpack utilise ces dépendances en tant que packages AMD.

Pour ce faire, compilez le code webpack en tant que bundle AMD et créez des alias de modules vers les URL de 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 le 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.

Documentation complémentaire

Récapitulatif

  • Activer le mode production si vous utilisez webpack 4
  • Minimisez votre code avec les options de réduction 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 balayage de l'arborescence
  • Compresser les images
  • Appliquer des optimisations spécifiques aux dépendances
  • Activer la concaténation des modules
  • Utilisez externals si c'est judicieux pour vous.