Zmniejsz rozmiar interfejsu

Jak użyć webpack, aby aplikacja była jak najmniejsza

Jedną z pierwszych rzeczy, jakie należy zrobić, jest zmniejszenie rozmiaru aplikacji do minimum. Oto jak to zrobić za pomocą webpack.

Używanie trybu produkcyjnego (dotyczy tylko webpacka 4)

W Webpacku 4 wprowadzono nowe oznaczenie mode. Możesz ustawić ten parametr na 'development' lub 'production', aby zasugerować webpackowi, że tworzysz aplikację na konkretne środowisko:

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

Podczas tworzenia wersji produkcyjnej aplikacji pamiętaj, aby włączyć tryb production. Spowoduje to, że webpack zastosuje optymalizacje, takie jak minifikacja i usuwanie kodu przeznaczonego tylko do celów testowych w bibliotekach i nie tylko.

Więcej informacji

Włącz kompresję

Minifikacja to kompresja kodu polegająca na usunięciu dodatkowych spacji, skróceniu nazw zmiennych itp. W ten sposób:

// 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 obsługuje 2 sposoby kompresji kodu: kompresję na poziomie pakietuopcje związane z ładowarką. Należy z nich korzystać jednocześnie.

Minifikacja na poziomie pakietu

Kompilacja na poziomie pakietu kompresuje cały pakiet po skompilowaniu. Działa to w następujący sposób:

  1. Napisany przez Ciebie kod wygląda tak:

    // comments.js
    import './comments.css';
    export function render(data, target) {
      console.log('Rendered!');
    }
    
  2. Webpack kompiluje je w takiej kolejności:

    // 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. Narzędzie do kompresji skompresuje go w ten sposób:

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

W webpack 4 kompresja na poziomie pakietu jest włączana automatycznie – zarówno w trybie produkcyjnym,jak i bez niego. W tle używa skraplaca UglifyJS. (jeśli chcesz wyłączyć kompresję, użyj trybu programowania lub prześlij parametr false do opcji optimization.minimize).

W webpack 3 musisz bezpośrednio używać wtyczki UglifyJS. Wtyczka jest dostarczana razem z webpack. Aby ją włączyć, dodaj ją do sekcji plugins konfiguracji:

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

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

Opcje dotyczące ładowarki

Drugim sposobem zminimalizowania kodu są opcje dotyczące konkretnego ładowacza (co to jest ładowacz). Opcje loadera umożliwiają kompresowanie elementów, których nie można zminifikować za pomocą minifikatora. Jeśli np. importujesz plik CSS z css-loader, plik jest kompilowany w ciąg znaków:

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

Kompaktor nie może skompresować tego kodu, ponieważ jest to ciąg znaków. Aby zminimalizować zawartość pliku, musimy skonfigurować loader:

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

Więcej informacji

Określ NODE_ENV=production

Innym sposobem zmniejszenia rozmiaru front-endu jest ustawienie w kodzie zmiennej środowiskowej NODE_ENV na wartość production.

Biblioteki odczytują zmienną NODE_ENV, aby wykryć, w jakim trybie powinny działać – w trybie rozwoju czy w produkcji. Niektóre biblioteki działają inaczej w zależności od tej zmiennej. Na przykład, gdy NODE_ENV nie jest ustawiony na production, Vue.js wykonuje dodatkowe sprawdzania i wypisuje ostrzeżenia:

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

React działa podobnie – wczytuje wersję deweloperską, która zawiera ostrzeżenia:

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

Takie kontrole i ostrzeżenia zwykle nie są potrzebne w wersji produkcyjnej, ale pozostają w kodzie i zwiększają rozmiar biblioteki. W webpack 4 usuń je,dodając opcję optimization.nodeEnv: 'production':

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

W webpack 3 użyj zamiast tego 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()
  ]
};

Zarówno opcja optimization.nodeEnv, jak i DefinePlugin działają w taki sam sposób – zastępują wszystkie wystąpienia ciągu process.env.NODE_ENV określonymi wartościami. W przypadku konfiguracji z powyższego przykładu:

  1. Webpack zastąpi wszystkie wystąpienia ciągu process.env.NODE_ENV ciągiem "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. Następnie minifikator usunie wszystkie takie gałęzie if, ponieważ "production" !== 'production' jest zawsze fałszywy, a wtyczka wie, że kod w tych gałęziach nigdy się nie wykona:

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

Więcej informacji

Korzystanie z modułów ES

Kolejnym sposobem na zmniejszenie rozmiaru front-endu jest użycie modułów ES.

Gdy używasz modułów ES, webpack może wykonywać usuwanie drzewa. Wyodrębnianie drzewa polega na przeszukiwaniu przez pakiet całego drzewa zależności, sprawdzaniu, które zależności są używane, i usuwaniu nieużywanych. Jeśli więc używasz składni modułu ES, webpack może wyeliminować nieużywany kod:

  1. Zapisujesz plik z wieloma eksportami, ale aplikacja używa tylko jednego z nich:

    // comments.js
    export const render = () => { return 'Rendered!'; };
    export const commentRestEndpoint = '/rest/comments';
    
    // index.js
    import { render } from './comments.js';
    render();
    
  2. Webpack rozumie, że commentRestEndpoint nie jest używany i nie generuje osobnego punktu eksportu w pakiecie:

    // 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. Minifikator usuwa nieużywaną zmienną:

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

Działa to nawet w przypadku bibliotek, jeśli są one napisane za pomocą modułów ES.

Nie musisz jednak używać wbudowanego w webpacka narzędzia do kompresji (UglifyJsPlugin). Do tego celu nadaje się dowolny narzędzie do kompresji, które obsługuje usuwanie nieużywanego kodu (np. wtyczka Babel Minify lub wtyczka Google Closure Compiler).

Więcej informacji

Zoptymalizuj obrazy

Obrazy stanowią ponad połowę rozmiaru strony. Chociaż nie są tak ważne jak JavaScript (np. nie blokują renderowania), nadal zajmują dużą część przepustowości. Użyj właściwości url-loader, svg-url-loaderimage-webpack-loader, aby zoptymalizować je w webpack.

url-loader wstawia małe pliki statyczne do aplikacji. Bez konfiguracji bierze przekazany plik, umieszcza go obok skompilowanego pakietu i zwraca adres URL tego pliku. Jeśli jednak określimy opcję limit, koduje pliki mniejsze niż ten limit jako adres URL danych w formacie Base64 i zwraca ten adres URL. W ten sposób obraz jest umieszczany w kodzie JavaScript i oszczędza żądanie 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 działa tak samo jak url-loader, z tym że koduje pliki za pomocą kodowania URL zamiast Base64. Jest to przydatne w przypadku obrazów SVG, ponieważ pliki SVG to po prostu zwykły tekst, więc takie kodowanie pozwala zmniejszyć rozmiar.

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

image-webpack-loader kompresuje obrazy, które przez niego przechodzą. Obsługuje obrazy JPG, PNG, GIF i SVG, więc będziemy go używać w przypadku wszystkich tych typów.

Ten ładowarkę nie umieszcza obrazów w aplikacji, więc musi ona współpracować z elementami url-loadersvg-url-loader. Aby uniknąć kopiowania i wklejania go w obu regułach (jednej dla obrazów JPG/PNG/GIF, a drugiej dla obrazów SVG), uwzględnimy ten ładowarkę jako osobną regułę z 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'
      }
    ]
  }
};

Domyślne ustawienia loadera są już gotowe do użycia, ale jeśli chcesz je dalej konfigurować, zapoznaj się z opcjami wtyczki. Aby wybrać opcje, zapoznaj się z doskonałym przewodnikiem Addy Osmani na temat optymalizacji obrazów.

Więcej informacji

Optymalizacja zależności

Ponad połowa średniego rozmiaru kodu JavaScript pochodzi z zależności, a część tego rozmiaru może być po prostu niepotrzebna.

Na przykład Lodash (w wersji 4.17.4) dodaje do pakietu 72 KB skompresowanego kodu. Jeśli jednak używasz tylko 20 metod, około 65 KB zaminifikowanego kodu nic nie robi.

Innym przykładem jest Moment.js. Wersja 2.19.1 zajmuje 223 KB skompresowanego kodu, co jest ogromną wartością – średni rozmiar kodu JavaScript na stronie w październiku 2017 r. wynosił 452 KB. Jednak 170 KB to pliki lokalizacji. Jeśli nie używasz Moment.js w wielu językach, te pliki będą niepotrzebnie zwiększać rozmiar pakietu.

Wszystkie te zależności można łatwo zoptymalizować. Metody optymalizacji zebraliśmy w repozytorium GitHub – odwiedź je.

Włącz łączenie modułów ES (zwane też podnoszeniem zakresu).

Podczas kompilowania pakietu webpack owija każdy moduł w funkcję:

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

Wcześniej było to konieczne, aby oddzielić od siebie moduły CommonJS/AMD. Jednak spowodowało to zwiększenie rozmiaru i obciążenie wydajnościowe każdego modułu.

Webpack 2 obsługuje moduły ES, które w odróżnieniu od modułów CommonJS i AMD mogą być grupowane bez owijania każdego z nich w funkcje. Pakowanie to jest możliwe dzięki konkatenacji modułów w webpack 3. Konkatenacja modułów polega na:

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

Widzicie różnicę? W zwykłym pakiecie moduł 0 wymagał render z modułu 1. W przypadku konkatenacji modułów element require jest po prostu zastępowany wymaganą funkcją, a moduł 1 zostaje usunięty. Pakiet zawiera mniej modułów i mniej danych modułów.

Aby włączyć to zachowanie, w webpack 4 włącz opcję optimization.concatenateModules:

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

W webpack 3 użyj ModuleConcatenationPlugin:

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

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

Więcej informacji

Użyj externals, jeśli masz kod webpack i kod inny niż webpack

Możesz mieć duży projekt, w którym część kodu jest kompilowana za pomocą webpack, a część nie. Na przykład witryna hostingowa, w której widżet odtwarzacza może być utworzony za pomocą webpacka, a strona otaczająca nie musi być:

Zrzut ekranu strony hostingu wideo
(zupełnie losowa witryna hostingowa wideo)

Jeśli oba fragmenty kodu mają wspólne zależności, możesz je udostępnić, aby uniknąć wielokrotnego pobierania kodu. Jest to możliwe dzięki opcji externals w webpacku – zastępuje ona moduły zmiennymi lub innymi importami zewnętrznymi.

Jeśli zależności są dostępne w window

Jeśli kod niebędący kodem Webpacka korzysta z zależności dostępnych jako zmienne w window, użyj aliasów nazw zależności do nazw zmiennych:

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

Przy tej konfiguracji webpack nie będzie tworzyć pakietów react i react-dom. Zamiast tego zostaną one zastąpione przez takie dane:

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

Jeśli zależności są ładowane jako pakiety AMD

Jeśli kod niebędący kodem webpack nie udostępnia zależności do window, sytuacja jest bardziej skomplikowana. Możesz jednak uniknąć wczytywania tego samego kodu dwukrotnie, jeśli kod niebędący kodem webpacka używa tych zależności jako pakietów AMD.

Aby to zrobić, skompiluj kod webpacka jako pakiet AMD i aliasuj moduły do adresów URL biblioteki:

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

Webpack spakuje pakiet do define() i spowoduje, że będzie on zależeć od tych adresów URL:

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

Jeśli kod inny niż webpack używa tych samych adresów URL do wczytywania zależności, pliki zostaną załadowane tylko raz – dodatkowe żądania będą korzystać z pamięci podręcznej programu ładującego.

Więcej informacji

Podsumowanie

  • Jeśli używasz webpacka 4, włącz tryb produkcyjny.
  • Minimalizowanie kodu za pomocą opcji kompresora i ładowarki na poziomie pakietu
  • Usuń kod przeznaczony tylko do celów testowych, zastępując NODE_ENV wartością production
  • Korzystanie z modułów ES do włączania potrząsania drzewem
  • Kompresowanie obrazów
  • Stosowanie optymalizacji dotyczących zależności
  • Włącz łączenie modułów
  • Użyj externals, jeśli ma to sens