Zmniejsz rozmiar interfejsu

Jak ograniczyć rozmiar aplikacji za pomocą pakietu internetowego

Jedną z pierwszych rzeczy, które należy zrobić przy optymalizowaniu aplikacji, jest jej jak najmniejsze. Oto jak to zrobić za pomocą pakietu webpack.

Używanie trybu produkcyjnego (tylko pakiet internetowy 4)

W pakiecie Webpack 4 wprowadziliśmy nową flagę mode. Możesz ustawić tę flagę na 'development' lub 'production', aby wskazać pakiet internetowy, że tworzysz aplikację do konkretnego środowiska:

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

Pamiętaj, aby podczas tworzenia wersji produkcyjnej aplikacji włączyć tryb production. Dzięki temu pakiet internetowy będzie optymalizował optymalizacje, np. minifikację, usunięcie z bibliotek kodu przeznaczonego tylko do programowania i inne.

Więcej informacji

Włącz minifikację

Minimalizacja 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 minifikacji kodu: minifikację na poziomie pakietu i opcje dotyczące modułów ładowania. Należy ich używać jednocześnie.

Minimalizacja na poziomie pakietu

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

  1. Piszesz taki kod:

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

    // 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. Minimalizator kompresuje ją do mniej więcej takiej postaci:

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

W pakiecie webpack 3 musisz bezpośrednio użyć wtyczki UglifyJS. Wtyczka jest w pakiecie z pakietem webpack. 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 ładowania

Drugim sposobem zmniejszenia kodu są opcje związane z ładowaniem (co to jest program ładujący). Opcje ładowania pozwalają kompresować elementy, których minifikator nie może zminimalizować. Jeśli na przykład zaimportujesz plik CSS za pomocą css-loader, zostanie on 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}",""]);

Minifier nie może skompresować tego kodu, ponieważ jest to ciąg znaków. Aby zmniejszyć zawartość pliku, musimy tak skonfigurować program ładowania:

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

Więcej informacji

Podaj NODE_ENV=production

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

Biblioteki odczytują zmienną NODE_ENV, aby określić, w jakim trybie powinny działać – w programie czy w środowisku produkcyjnym. W zależności od tej zmiennej niektóre biblioteki zachowują się inaczej. Jeśli na przykład NODE_ENV nie ma wartości production, Vue.js wykonuje dodatkowe testy 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.');
}
// …

Reakcja działa podobnie – wczytuje kompilację rozwojową, 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 są zwykle niepotrzebne w środowisku produkcyjnym, ale pozostają w kodzie i zwiększają rozmiar biblioteki. W pakiecie internetowym 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 użyj zamiast niego 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 ten sam sposób – zastępują wszystkie wystąpienia process.env.NODE_ENV określoną wartością. W konfiguracji z powyższej konfiguracji:

  1. Pakiet internetowy zastąpi wszystkie wystąpienia parametru process.env.NODE_ENV elementem "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. A potem minifikator usunie wszystkie takie gałęzie (if), bo "production" !== 'production' ma zawsze wartość false (fałsz), a wtyczka rozumie, że kod wewnątrz tych gałęzi 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

Następnym sposobem na zmniejszenie rozmiaru interfejsu jest użycie modułów ES.

Po użyciu modułów ES pakiet internetowy może trzęsić się drzewem. Potrząsanie drzewem ma miejsce, gdy narzędzie do łączenia pakietów przemierza całe drzewo zależności, sprawdza używane zależności i usuwa nieużywane. Jeśli używasz składni modułu ES, pakiet internetowy może wyeliminować nieużywany kod:

  1. Piszesz 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 dyrektywa commentRestEndpoint nie jest używana 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. Minimalizator 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 z bibliotekami, które są tworzone za pomocą modułów ES.

Nie musisz jednak używać dokładnie minifikatora wbudowanego w pakiet internetowy (UglifyJsPlugin). Wystarczy użyć dowolnego narzędzia do usuwania martwego kodu (np. wtyczki Babel Minify lub Google Closure Compiler).

Więcej informacji

Zoptymalizuj obrazy

Obrazy zajmują ponad połowę rozmiaru strony. Chociaż nie są tak ważne jak JavaScript (np. nie blokują renderowania), nadal pochłaniają dużą część przepustowości. Użyj atrybutów url-loader, svg-url-loader i image-webpack-loader, aby je zoptymalizować w pakiecie internetowym.

url-loader integruje w aplikację małe pliki statyczne. Bez konfiguracji pobiera przekazywany plik, umieszcza go obok skompilowanego pakietu i zwraca jego adres URL. Jeśli jednak określimy opcję limit, pliki mniejsze niż ten limit zostaną zakodowane jako adres URL danych Base64 i zwróci ten adres. Spowoduje to wbudowanie obrazu w kod JavaScript i zapisanie żądania 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 adresów URL zamiast w formacie Base64. Jest to przydatne w przypadku obrazów SVG – pliki SVG to po prostu zwykły tekst, więc to kodowanie jest bardziej efektywne pod względem rozmiaru.

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

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

Ten program ładujący nie zawiera obrazów w aplikacji, więc musi działać w parze z parametrami url-loader i svg-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), dodamy ten moduł ładowania jako oddzielną regułę w 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 programu ładującego są już gotowe, ale jeśli chcesz je skonfigurować, zapoznaj się z opcjami wtyczek. Aby wybrać opcje, które możesz określić, zapoznaj się ze znakomitym przewodnikiem Addy Osmani na temat optymalizacji obrazów.

Więcej informacji

Optymalizacja zależności

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

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

Kolejny przykład to Moment.js. Opublikowana w niej wersja 2.19.1 zajmuje 223 KB zminifikowanego kodu, co jest olbrzymią ilością miejsca – średni rozmiar JavaScriptu na stronie w październiku 2017 r. wynosił 452 KB. Jednak 170 KB tego rozmiaru to pliki lokalizacyjne. Jeśli nie używasz Moment.js w wielu językach, pliki te spowalniają pakiet bez powodu.

Wszystkie te zależności można łatwo zoptymalizować. Wskazówki dotyczące optymalizacji znajdziesz w repozytorium GitHub – sprawdź je.

Włącz konkatenację modułów ES (nazywanej też podnoszeniem zakresu)

Gdy tworzysz pakiet, pakiet internetowy pakuje 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!');
  }
})

W przeszłości było to wymagane do odizolowania od siebie modułów CommonJS/AMD. Wiązało się to jednak z dodatkowymi kosztami dotyczącymi rozmiaru i wydajności każdego modułu.

W pakiecie Webpack 2 wprowadziliśmy obsługę modułów ES, które w przeciwieństwie do modułów CommonJS i AMD można łączyć w pakiety bez dodawania do każdego z nich funkcji. Takie grupowanie jest możliwe dzięki konkatenacji modułów. Jak działa konkatenacja modułów:

// 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ł uprawnienia render z modułu 1. Dzięki konkatenacji modułów funkcja require zostaje po prostu zastępowana wymaganą funkcją, a moduł 1 usuwany. Pakiet ma mniej modułów i mniej nadpisywania modułów.

Aby włączyć tę funkcję, w pakiecie webpack 4 włącz opcję optimization.concatenateModules:

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

W pakiecie internetowym 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 zarówno kod pakietu Webpack, jak i kod spoza tego pakietu

Możesz mieć duży projekt, w którym część kodu jest skompilowana z pakietem webpack, a część nie. Podobnie jak w przypadku witryny hostingu filmów, w której widżet odtwarzacza może być utworzony za pomocą pakietu internetowego, a otaczająca go strona może nie mieć następujących wartości:

Zrzut ekranu witryny hostingu filmów
(całkowicie losowa witryna hostująca filmy)

Jeśli oba fragmenty kodu są powiązane, możesz je udostępniać, aby uniknąć wielokrotnego pobierania kodu. Użyjesz do tego opcji externals pakietu internetowego – zastępuje ona moduły ze zmiennymi lub innymi importami zewnętrznymi.

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

Jeśli Twój kod spoza pakietu internetowego zależy od zależności dostępnych jako zmienne w zasadzie window, aliasy nazw zależności do nazw zmiennych:

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

Przy tej konfiguracji pakiet internetowy nie będzie łączyć w pakiety react i react-dom pakietów. Zostaną one zastąpione czymś podobnym:

// 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 Twój kod inny niż internetowy pakiet nie ujawnia zależności w usłudze window, sprawa się komplikuje. Nadal możesz jednak uniknąć dwukrotnego wczytywania tego samego kodu, jeśli kod spoza pakietu internetowego wykorzystuje te zależności jako pakiety AMD.

W tym celu skompiluj kod pakietu internetowego jako pakiet AMD i moduły aliasów 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 zapakuje pakiet w kod define() i umożliwi jego podłączenie do tych adresów URL:

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

Jeśli kod spoza pakietu internetowego korzysta z tych samych adresów URL do wczytywania zależności, te pliki są wczytywane tylko raz – dodatkowe żądania będą korzystać z pamięci podręcznej programu wczytującego.

Więcej informacji

Podsumowanie

  • Jeśli używasz pakietu webpack 4, włącz tryb produkcyjny
  • Zminimalizuj kod dzięki opcji minifikacji i wczytywania na poziomie pakietu
  • Usuń kod przeznaczony tylko do programowania, zastępując fragment NODE_ENV kodem production
  • Włączanie potrząsania drzew za pomocą modułów ES
  • Kompresuj obrazy
  • Zastosuj optymalizacje zależnie od zależności
  • Włącz konkatenację modułów
  • Jeśli jest to dla Ciebie przydatne, użyj externals