Réduire la taille de l'interface

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

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 créez l'application pour un environnement spécifique:

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

Veillez à activer le mode production lorsque vous créez votre application pour la production. webpack appliquera alors des optimisations telles que la minification, la suppression du code réservé au développement dans les bibliothèques, et plus encore.

Documentation complémentaire

Activer la minification

La minification consiste à compresser le code en supprimant les espaces supplémentaires, 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 propose deux façons de réduire le code: la minimisation au niveau du groupe 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 le code suivant:

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

    // 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 minification le compresse pour obtenir à peu près ce résultat:

    // 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 minificateur UglifyJS en arrière-plan. (Si vous devez désactiver la minification, il vous suffit d'utiliser le mode de développement ou de transmettre 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). Avec les options de chargeur, vous pouvez compresser des éléments que le minificateur ne peut pas minifier. Par exemple, lorsque vous importez un fichier CSS avec css-loader, le fichier est compilé en 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}",""]);

Le minificateur ne peut pas compresser ce code, car il s'agit d'une chaîne. Pour minifier le contenu du fichier, nous devons configurer le chargeur comme suit:

// 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 du front-end 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 dans quel mode 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 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 : 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.'
);
// …

De tels contrôles et avertissements ne sont généralement pas nécessaires 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 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". Le tree-shaking consiste à faire traverser l'ensemble de l'arborescence des dépendances par un bundler, à vérifier quelles dépendances sont utilisées et à supprimer celles qui ne le sont pas. Par conséquent, si vous utilisez la syntaxe de module ES, webpack peut supprimer 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). N'importe quel outil de minification compatible avec la suppression du code mort (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. Cela est utile pour les images SVG. Étant donné que les fichiers SVG ne sont que du 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 avec url-loader et svg-url-loader. Pour éviter de le copier-coller dans les deux règles (l'une pour les images JPG/PNG/GIF et l'autre pour les images SVG), nous inclurons 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 à l'emploi. Toutefois, si vous souhaitez le configurer plus en détail, 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 de cette taille correspondent à 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. Découvrez-le !

Activer la concatenaison de modules pour les modules ES (également appelé "hoisting de portée")

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

Voyez-vous la différence ? Dans le bundle simple, le module 0 nécessitait render du module 1. Avec la concaténation de 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, 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()
  ]
};

Documentation complémentaire

Utilisez externals si vous disposez à la fois de code webpack et de code non webpack.

Vous pouvez avoir un projet volumineux dans lequel certains codes sont compilés avec webpack et d'autres non. Comme un site d'hébergement de vidéos, où le widget du lecteur peut être créé avec webpack, et la page environnante peut ne pas l'ê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 parties 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 regroupera pas les packages react et react-dom. 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 non webpack n'expose pas de 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 consomme ces dépendances en tant que paquets 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 non 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
  • Minimiser votre code avec les options de minificateur 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 de modules
  • Utilisez externals si cela vous convient.