Front-End-Größe verringern

Wie du mit Webpack deine App so klein wie möglich gestalten kannst

Einer der ersten Schritte bei der Optimierung einer Anwendung besteht darin, sie so klein wie möglich zu machen. So gehts mit Webpack.

Produktionsmodus verwenden (nur Webpack 4)

Webpack 4 hat das neue Flag mode eingeführt. Sie können dieses Flag auf 'development' oder 'production' setzen, um Webpack darauf hinzuweisen, dass Sie die Anwendung für eine bestimmte Umgebung erstellen:

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

Achte darauf, den production-Modus zu aktivieren, wenn du deine App für die Produktion erstellst. Dadurch wendet Webpack Optimierungen wie die Reduzierung von Code in Bibliotheken und mehr an, der nur für die Entwicklung vorgesehen ist.

Weitere Informationen

Reduzierung aktivieren

Bei der Reduzierung wird der Code komprimiert, indem u. a. zusätzliche Leerzeichen entfernt oder Variablennamen gekürzt werden. Ein Beispiel:

// 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 unterstützt zwei Möglichkeiten zum Reduzieren des Codes: die Komprimierung auf Bundle-Ebene und loader-spezifische Optionen. Sie sollten gleichzeitig verwendet werden.

Reduzierung auf Bundle-Ebene

Durch die Reduzierung auf Bundle-Ebene wird das gesamte Bundle nach der Kompilierung komprimiert. Das geht so:

  1. Sie schreiben Code wie folgt:

    // comments.js
    import './comments.css';
    export function render(data, target) {
      console.log('Rendered!');
    }
    
  2. Webpack kompiliert es in etwa so:

    // 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. Die Komprimierung erfolgt in etwa so:

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

In Webpack 4 ist die Reduzierung auf Bundle-Ebene automatisch aktiviert – sowohl im Produktionsmodus als auch ohne. Es nutzt den UglifyJS-Minifier im Hintergrund. Wenn Sie die Minimierung deaktivieren müssen, verwenden Sie einfach den Entwicklungsmodus oder übergeben Sie false an die Option optimization.minimize.

In Webpack 3 müssen Sie das UglifyJS-Plug-in direkt verwenden. Das Plug-in ist im Webpack enthalten. Um es zu aktivieren, müssen Sie es dem Abschnitt plugins der Konfiguration hinzufügen:

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

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

Loader-spezifische Optionen

Die zweite Möglichkeit, den Code zu komprimieren, sind loader-spezifische Optionen (was ein Ladeprogramm ist). Mit den Loader-Optionen können Sie Elemente komprimieren, die sich nicht komprimieren lassen. Wenn Sie beispielsweise eine CSS-Datei mit css-loader importieren, wird die Datei in einen String kompiliert:

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

Der Code kann von der Minifier nicht komprimiert werden, da er ein String ist. Um den Dateiinhalt zu reduzieren, müssen wir den Loader so konfigurieren:

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

Weitere Informationen

Geben Sie NODE_ENV=production an

Eine weitere Möglichkeit, die Frontend-Größe zu verringern, besteht darin, die Umgebungsvariable NODE_ENV im Code auf den Wert production festzulegen.

Bibliotheken lesen die Variable NODE_ENV, um zu ermitteln, in welchem Modus sie arbeiten sollten – in der Entwicklungs- oder in der Produktion. Einige Bibliotheken verhalten sich aufgrund dieser Variablen anders. Wenn NODE_ENV beispielsweise nicht auf production gesetzt ist, führt Vue.js zusätzliche Prüfungen durch und gibt Warnungen aus:

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

React funktioniert ähnlich – es wird ein Entwicklungs-Build geladen, der die folgenden Warnungen enthält:

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

Solche Prüfungen und Warnungen sind in der Produktion in der Regel nicht erforderlich. Sie verbleiben jedoch im Code und erhöhen die Größe der Bibliothek. Entfernen Sie sie in Webpack 4,indem Sie die Option optimization.nodeEnv: 'production' hinzufügen:

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

Verwende in Webpack 3 stattdessen 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()
  ]
};

Die Option optimization.nodeEnv und DefinePlugin funktionieren auf die gleiche Weise – sie ersetzen alle Vorkommen von process.env.NODE_ENV durch den angegebenen Wert. Führen Sie die obige Konfiguration aus:

  1. Webpack ersetzt alle Vorkommen von process.env.NODE_ENV durch "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. Anschließend entfernt der Minifier alle entsprechenden if-Zweige, da "production" !== 'production' immer falsch ist und das Plug-in versteht, dass der Code in diesen Zweigen niemals ausgeführt wird:

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

Weitere Informationen

ES-Module verwenden

Die nächste Möglichkeit, die Frontend-Größe zu verringern, besteht in der Verwendung von ES-Modulen.

Wenn Sie ES-Module verwenden, kann Webpack das Tree Shaking übernehmen. Baumwackeln tritt auf, wenn ein Bundler den gesamten Abhängigkeitsbaum durchläuft, überprüft, welche Abhängigkeiten verwendet werden, und nicht verwendete Abhängigkeiten entfernt. Wenn Sie also die ES-Modulsyntax verwenden, kann Webpack den nicht verwendeten Code entfernen:

  1. Sie schreiben eine Datei mit mehreren Exporten, aber die Anwendung verwendet nur einen davon:

    // comments.js
    export const render = () => { return 'Rendered!'; };
    export const commentRestEndpoint = '/rest/comments';
    
    // index.js
    import { render } from './comments.js';
    render();
    
  2. Webpack versteht, dass commentRestEndpoint nicht verwendet wird, und generiert im Bundle keinen separaten Exportpunkt:

    // 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. Der Minifier entfernt die nicht verwendete Variable:

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

Dies funktioniert auch mit Bibliotheken, wenn diese mit ES-Modulen geschrieben wurden.

Sie müssen jedoch nicht den genauen integrierten Minifier von Webpack (UglifyJsPlugin) verwenden. Dazu können Sie jeden Minifier, der die Entfernung von inaktivem Code unterstützt (z.B. Babel Minify-Plug-in oder Google Closure Compiler-Plug-in) unterstützt.

Weitere Informationen

Bilder optimieren

Bilder machen mehr als die Hälfte der Seitengröße aus. Sie sind zwar nicht so wichtig wie JavaScript (sie blockieren das Rendering), verbrauchen aber dennoch einen großen Teil der Bandbreite. Verwende url-loader, svg-url-loader und image-webpack-loader, um sie im Webpack zu optimieren.

Mit url-loader werden kleine statische Dateien in die Anwendung inline eingefügt. Ohne Konfiguration wird eine übergebene Datei genommen, neben das kompilierte Bundle platziert und die URL dieser Datei zurückgegeben. Wenn wir jedoch die Option limit angeben, werden Dateien, die kleiner sind, als Base64-Daten-URL codiert und diese URL wird zurückgegeben. Dadurch wird das Bild in den JavaScript-Code eingebettet und eine HTTP-Anfrage gespeichert:

// 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 funktioniert wie url-loader, mit der Ausnahme, dass Dateien mit der URL-Codierung statt mit der Base64-Codierung codiert werden. Dies ist nützlich für SVG-Bilder, da SVG-Dateien nur aus Text bestehen und diese Codierung eine größere Größenoptimierung ermöglicht.

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

Mit image-webpack-loader werden die durchlaufenen Images komprimiert. Es unterstützt JPG-, PNG-, GIF- und SVG-Bilder, sodass wir es für alle diese Typen verwenden werden.

Dieses Ladeprogramm bettet keine Bilder in die App ein. Daher muss es zusammen mit url-loader und svg-url-loader funktionieren. Damit es nicht kopiert und in beide Regeln (eine für JPG-/PNG-/GIF-Bilder und eine für SVG-Bilder) eingefügt wird, fügen wir dieses Ladeprogramm als separate Regel mit enforce: 'pre' ein:

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

Die Standardeinstellungen des Ladeprogramms sind bereits festgelegt. Wenn Sie die Einstellungen weiter anpassen möchten, sehen Sie sich die Plug-in-Optionen an. Informationen zur Auswahl der Optionen finden Sie in Addy Osmanis ausgezeichnetem Leitfaden zur Bildoptimierung.

Weitere Informationen

Abhängigkeiten optimieren

Mehr als die Hälfte der durchschnittlichen JavaScript-Größe stammt aus Abhängigkeiten, und ein Teil dieser Größe ist möglicherweise einfach unnötig.

Lodash (ab Version 4.17.4) fügt dem Bundle zum Beispiel 72 KB komprimierten Code hinzu. Wenn Sie jedoch nur 20 der Methoden verwenden, werden ca. 65 KB an reduziertem Code nichts.

Ein weiteres Beispiel ist Moment.js. Die Version 2.19.1 benötigt 223 KB reduzierten Code. Das ist eine enorme Summe. Die durchschnittliche Größe von JavaScript auf einer Seite betrug im Oktober 2017 452 KB. 170 KB dieser Größe sind jedoch Lokalisierungsdateien. Wenn Sie Moment.js nicht mit mehreren Sprachen verwenden, blähen diese Dateien das Bundle ohne Zweck auf.

All diese Abhängigkeiten können einfach optimiert werden. Wir haben Optimierungsansätze in einem GitHub-Repository zusammengestellt. Weitere Informationen

Modulverkettung für ES-Module aktivieren (auch Bereichshub)

Wenn Sie ein Bundle erstellen, verpackt Webpack jedes Modul in eine Funktion:

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

In der Vergangenheit war dies erforderlich, um CommonJS/AMD-Module voneinander zu isolieren. Dies führte jedoch zu zusätzlichem Umfang und Leistungsaufwand für jedes Modul.

Webpack 2 unterstützt jetzt ES-Module, die im Gegensatz zu CommonJS- und AMD-Modulen gebündelt werden können, ohne jedes mit einer Funktion zu umschließen. Und Webpack 3 ermöglichte eine solche Bündelung – mit der Modulverkettung. Die Modulverkettung bewirkt Folgendes:

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

Sehen Sie den Unterschied? Im einfachen Bundle erforderte Modul 0 render aus Modul 1. Bei der Modulverkettung wird require einfach durch die erforderliche Funktion ersetzt und Modul 1 entfernt. Das Bundle enthält weniger Module – und weniger Modul-Overhead!

Aktivieren Sie in Webpack 4 die Option optimization.concatenateModules, um dieses Verhalten zu aktivieren:

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

Verwende in Webpack 3 den ModuleConcatenationPlugin:

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

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

Weitere Informationen

Verwende externals, wenn du sowohl Webpack- als auch Nicht-Webpack-Code hast

Vielleicht haben Sie ein großes Projekt, bei dem mit Webpack nur ein Teil des Codes kompiliert wurde. Wie bei einer Videohosting-Website, bei der das Player-Widget mit Webpack und die umgebende Seite möglicherweise nicht erstellt wurde:

Screenshot einer Videohosting-Website
(Eine vollständig zufällige Website zum Hosten von Videos)

Wenn beide Codeteile gemeinsame Abhängigkeiten haben, können Sie sie freigeben, um zu vermeiden, dass der Code mehrmals heruntergeladen wird. Dazu verwenden Sie die Option externals des Webpacks. Sie ersetzt Module durch Variablen oder andere externe Importe.

Wenn Abhängigkeiten in window verfügbar sind

Wenn der Nicht-Webpack-Code von Abhängigkeiten abhängig ist, die in window als Variablen verfügbar sind, geben Sie Abhängigkeitsnamen einen Alias für Variablennamen:

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

Mit dieser Konfiguration bündelt Webpack keine react- und react-dom-Pakete. Stattdessen werden sie etwa so ersetzt:

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

Wenn Abhängigkeiten als AMD-Pakete geladen werden

Wenn der Code, der nicht aus dem Webpack stammt, keine Abhängigkeiten in window bereitstellt, ist die Sache komplizierter. Sie können jedoch trotzdem vermeiden, denselben Code zweimal zu laden, wenn der Nicht-Webpack-Code diese Abhängigkeiten als AMD-Pakete verwendet.

Kompilieren Sie dazu den Webpack-Code als AMD-Bundle und Aliasmodule zu Bibliotheks-URLs:

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

Webpack packt das Bundle in define() ein und macht es von diesen URLs abhängig:

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

Wenn der Nicht-Webpack-Code dieselben URLs zum Laden der Abhängigkeiten verwendet, werden diese Dateien nur einmal geladen. Zusätzliche Anfragen nutzen den Lade-Cache.

Weitere Informationen

Zusammenfassung

  • Produktionsmodus aktivieren, wenn Sie Webpack 4 verwenden
  • Code mit den Komprimierungs- und Ladeprogrammoptionen auf Bundle-Ebene minimieren
  • Entfernen Sie den Entwicklercode, indem Sie NODE_ENV durch production ersetzen.
  • Baumschütteln mit ES-Modulen aktivieren
  • Bilder komprimieren
  • Abhängigkeitsspezifische Optimierungen anwenden
  • Modulverkettung aktivieren
  • Verwende externals, wenn dies für dich sinnvoll ist