Zmniejsz rozmiar interfejsu

Jak za pomocą pakietu internetowego zmniejszyć aplikację do rozmiaru

Jedną z pierwszych rzeczy, jakie należy zrobić przy optymalizacji aplikacji, jest jej jak najmniejszy rozmiar. Oto jak to zrobić w pakiecie internetowym.

Webpack 4 wprowadził nowy parametr mode. Możesz ustawić tę flagę na 'development' lub 'production', aby wskazać, że kompilujesz aplikację dla konkretnego środowiska:

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

Podczas tworzenia aplikacji w wersji produkcyjnej włącz tryb production. Dzięki temu pakiet internetowy będzie stosować optymalizacje, takie jak minifikacja, usuwanie z bibliotek kodu tylko dla programistów, i inne.

Więcej informacji

Włącz minifikację

Zmniejszenie polega na skompresowaniu kodu poprzez usunięcie dodatkowych spacji, skrócenie 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 ich używać jednocześnie.

Minimalizacja na poziomie pakietu

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

  1. Kod wygląda tak:

    // comments.js
    import './comments.css';
    export function render(data, target) {
      console.log('Rendered!');
    }
    
  2. Webpack kompiluje go do mniej więcej takiego kodu:

    // 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 danych kompresuje je 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 pakiecie internetowym 4 minifikacja na poziomie pakietu jest włączona automatycznie – zarówno w trybie produkcyjnym,jak i bez niego. Wykorzystuje w nim minifier UglifyJS. Jeśli kiedykolwiek zechcesz wyłączyć minifikację, użyj trybu programisty lub przekaż false do opcji optimization.minimize.

W pakiecie webpack 3 musisz bezpośrednio użyć wtyczki UglifyJS. Do wtyczki jest dołączony pakiet internetowy. Aby go włączyć, dodaj go do sekcji plugins konfiguracji:

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

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

Opcje dotyczące modułu ładowania

Drugim sposobem na zminimalizowanie kodu są opcje dla konkretnego narzędzia (co to jest program ładujący). Opcje loadera umożliwiają kompresowanie elementów, których nie można zminifikować za pomocą minifikatora. Jeśli na przykład importujesz plik CSS za pomocą css-loader, plik jest skompilowany 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 w ten sposób:

// 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 interfejsu jest ustawienie NODE_ENV zmiennej środowiskowej w kodzie na wartość production.

Biblioteki odczytują zmienną NODE_ENV, aby określić, w którym trybie powinny działać – w wersji deweloperskiej czy produkcyjnej. Niektóre biblioteki działają inaczej w zależności od tej zmiennej. Jeśli na przykład NODE_ENV nie ma wartości production, Vue.js wykonuje dodatkowe sprawdzanie i wyświetla 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 kompilację 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 pakiecie webpack 3 zamiast tego użyj 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 minifier usuwa wszystkie takie gałęzie if, ponieważ "production" !== 'production' ma zawsze wartość fałsz, a wtyczka rozumie, że kod zawarty w tych gałęziach nigdy nie zostanie wykonany:

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

Innym sposobem na zmniejszenie rozmiaru interfejsu 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). Każdy minifilar, który obsługuje usuwanie martwego kodu (np. wtyczka Babel Minify lub Google Closure Compiler), wystarczy.

Więcej informacji

Zoptymalizuj obrazy

Obrazy zajmują ponad połowę rozmiaru strony. Nie są one tak ważne jak JavaScript (np. nie blokują renderowania), ale i tak zużywają dużą część przepustowości. Użyj narzędzi url-loader, svg-url-loader i image-webpack-loader, aby je zoptymalizować w pakiecie internetowym.

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, pliki mniejsze niż ten limit będą zakodowane jako adres URL danych w Base64 i zwrócą 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 tą różnicą, że koduje pliki za pomocą kodowania URL zamiast Base64. Jest to przydatne w przypadku obrazów SVG, ponieważ pliki SVG to tylko zwykły tekst, a 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 przechodzą przez niego. Plik obsługuje obrazy JPG, PNG, GIF i SVG, więc będziemy go używać we wszystkich tych formatach.

Ten program ładujący nie umieszcza obrazów w aplikacji, dlatego musi działać w parze z url-loader i svg-url-loader. Aby uniknąć kopiowania go i wklejania do obu reguł (jednej dla obrazów JPG/PNG/GIF i drugiej dla obrazów SVG), dodamy ten moduł ładujący 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'
      }
    ]
  }
};

Możesz już zacząć korzystać z domyślnych ustawień programu wczytującego, ale jeśli chcesz skonfigurować je bardziej szczegółowo, sprawdź opcje wtyczki. Aby wybrać opcje, zapoznaj się ze świetnym przewodnikiem Addy Osmaniego na temat optymalizacji obrazów.

Więcej informacji

Optymalizacja zależności

Ponad połowa średniego rozmiaru kodu JavaScript zależy od zależności, a część tego rozmiaru może być po prostu zbędna.

Na przykład Lodash (w wersji 4.17.4) dodaje do pakietu 72 KB skompresowanego kodu. Jeśli jednak zastosujesz tylko 20 metod, ok. 65 KB zminifikowanego kodu nie będzie działać.

Innym przykładem jest plik 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 tego rozmiaru 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ć. Zebraliśmy w repozytorium GitHub metody optymalizacji – zapoznaj się z nim.

Włączanie konkatenacji modułów w przypadku modułów ES (inaczej nazywane rozszerzanie zakresu)

Podczas tworzenia pakietu pakiet internetowy opakowuje każdy moduł w jedną 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. Wiąże się to jednak ze wzrostem rozmiaru i wydajności 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();
})

Widzisz 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 składa się z mniejszej liczby modułów i mniejszego nakładu pracy.

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 kodu externals, jeśli masz zarówno kod pakietu internetowego, jak i innego

Możesz mieć duży projekt, w którym część kodu jest kompilowana za pomocą webpack, a część nie. Podobnie jak w przypadku witryny hostującej filmy, w której widżet odtwarzacza może być utworzony za pomocą pakietu webpack, a otaczająca go strona nie może:

Zrzut ekranu witryny hostującej filmy
(całkowicie losowa witryna z filmami)

Jeśli oba fragmenty kodu mają wspólne zależności, możesz je udostępnić, aby uniknąć wielokrotnego pobierania ich 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'
  }
};

W tej konfiguracji webpack nie będzie łączyć 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ą wczytywane 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.

W tym celu skompiluj kod pakietu internetowego jako pakiet AMD i moduły aliasów dla adresów URL bibliotek:

// 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
  • Użyj modułów ES, aby umożliwić potrząsanie drzewem
  • Kompresowanie obrazów
  • Zastosuj optymalizacje dostosowane do zależności
  • Włącz konkatenację modułów
  • Jeśli uważasz, że to może być przydatne, użyj metody externals