Уменьшить размер интерфейса

Как использовать веб-пакет, чтобы сделать ваше приложение как можно меньшим

Первое, что нужно сделать при оптимизации приложения, — сделать его как можно меньше. Вот как это сделать с помощью веб-пакета.

Используйте производственный режим (только для Webpack 4)

В Webpack 4 появился новый флаг mode . Вы можете установить для этого флага значение 'development' или 'production' , чтобы намекнуть веб-пакету, что вы создаете приложение для определенной среды:

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

Обязательно включите production режим при создании приложения для производства. Это заставит вебпак применять такие оптимизации, как минимизация, удаление кода, предназначенного только для разработки, в библиотеках и многое другое .

Дальнейшее чтение

Включить минификацию

Минимизация — это сжатие кода путем удаления лишних пробелов, сокращения имен переменных и т. д. Так:

// 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 поддерживает два способа минимизации кода: минификация на уровне пакета и параметры, специфичные для загрузчика . Их следует использовать одновременно.

Минимизация на уровне пакета

Минимизация на уровне пакета сжимает весь пакет после компиляции. Вот как это работает:

  1. Вы пишете такой код:

    // comments.js
    import './comments.css';
    export function render(data, target) {
      console.log('Rendered!');
    }
    
  2. Webpack компилирует его примерно в следующее:

    // 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. Минификатор сжимает его примерно так:

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

В webpack 4 минификация на уровне бандла включается автоматически — как в рабочем режиме, так и без него. Под капотом он использует минификатор UglifyJS . (Если вам когда-нибудь понадобится отключить минимизацию, просто используйте режим разработки или передайте false опции optimization.minimize .)

В веб-пакете 3 вам необходимо напрямую использовать плагин UglifyJS . Плагин поставляется в комплекте с веб-пакетом; чтобы включить его, добавьте его в раздел plugins конфигурации:

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

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

Параметры, зависящие от загрузчика

Второй способ минимизации кода — параметры, специфичные для загрузчика ( что такое загрузчик ). С помощью параметров загрузчика вы можете сжимать то, что минификатор не может минимизировать. Например, когда вы импортируете файл CSS с помощью css-loader , файл компилируется в строку:

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

Минификатор не может сжать этот код, поскольку это строка. Чтобы минимизировать содержимое файла, нам нужно настроить загрузчик следующим образом:

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

Дальнейшее чтение

Укажите NODE_ENV=production

Другой способ уменьшить размер внешнего интерфейса — установить для переменной среды NODE_ENV в вашем коде значение production .

Библиотеки считывают переменную NODE_ENV , чтобы определить, в каком режиме им следует работать — в рабочем или в рабочем. Некоторые библиотеки ведут себя по-разному в зависимости от этой переменной. Например, если для NODE_ENV не установлено production , Vue.js выполняет дополнительные проверки и выводит предупреждения:

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

React работает аналогично — он загружает сборку разработки, которая включает предупреждения:

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

Такие проверки и предупреждения обычно не нужны в продакшене, но они остаются в коде и увеличивают размер библиотеки. В веб-пакете 4 удалите их, добавив параметр optimization.nodeEnv: 'production' :

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

В веб-пакете 3 вместо этого используйте 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()
  ]
};

И опция optimization.nodeEnv , и DefinePlugin работают одинаково — они заменяют все вхождения process.env.NODE_ENV указанным значением. С конфигом сверху:

  1. Webpack заменит все process.env.NODE_ENV на "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. И тогда минификатор удалит все такие ветки if — потому что "production" !== 'production' всегда ложно, и плагин понимает, что код внутри этих веток никогда не выполнится:

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

Дальнейшее чтение

Используйте ES-модули

Следующий способ уменьшить размер внешнего интерфейса — использовать ES-модули .

Когда вы используете модули ES, веб-пакет может выполнять встряхивание дерева. Tree-shaking — это когда сборщик обходит все дерево зависимостей, проверяет, какие зависимости используются, и удаляет неиспользуемые. Итак, если вы используете синтаксис модуля ES, веб-пакет может удалить неиспользуемый код:

  1. Вы пишете файл с несколькими экспортами, но приложение использует только один из них:

    // comments.js
    export const render = () => { return 'Rendered!'; };
    export const commentRestEndpoint = '/rest/comments';
    
    // index.js
    import { render } from './comments.js';
    render();
    
  2. Webpack понимает, что commentRestEndpoint не используется и не создает в бандле отдельную точку экспорта:

    // 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. Минификатор удаляет неиспользуемую переменную:

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

Это работает даже с библиотеками, если они написаны с использованием ES-модулей.

Однако вам не обязательно использовать именно встроенный минификатор веб-пакета ( UglifyJsPlugin ). Любой минификатор, который поддерживает удаление мертвого кода (например, плагин Babel Minify или плагин Google Closure Compiler ), подойдет.

Дальнейшее чтение

Оптимизация изображений

Изображения занимают более половины размера страницы. Хотя они не так критичны, как JavaScript (например, не блокируют рендеринг), они все же потребляют большую часть пропускной способности. Используйте url-loader , svg-url-loader и image-webpack-loader чтобы оптимизировать их в веб-пакете.

url-loader встраивает в приложение небольшие статические файлы. Без настройки он берет переданный файл, помещает его рядом с скомпилированным пакетом и возвращает URL-адрес этого файла. Однако, если мы укажем параметр limit , он будет кодировать файлы, меньшие этого предела, как URL-адрес данных Base64 и возвращать этот URL-адрес. Это встраивает изображение в код JavaScript и сохраняет 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 работает так же, как url-loader , за исключением того, что он кодирует файлы с использованием кодировки URL вместо кодировки Base64. Это полезно для изображений SVG: поскольку файлы SVG представляют собой обычный текст, такая кодировка более эффективна по размеру.

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

image-webpack-loader сжимает проходящие через него изображения. Он поддерживает изображения JPG, PNG, GIF и SVG, поэтому мы собираемся использовать его для всех этих типов.

Этот загрузчик не встраивает изображения в приложение, поэтому он должен работать в паре с url-loader и svg-url-loader . Чтобы избежать копирования его в оба правила (одно для изображений JPG/PNG/GIF, а другое для изображений SVG), мы включим этот загрузчик как отдельное правило с 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'
      }
    ]
  }
};

Настройки загрузчика по умолчанию уже готовы, но если вы хотите настроить их дальше, см. параметры плагина . Чтобы выбрать, какие параметры указать, ознакомьтесь с отличным руководством Адди Османи по оптимизации изображений .

Дальнейшее чтение

Оптимизация зависимостей

Более половины среднего размера JavaScript приходится на зависимости, и часть этого размера может быть просто ненужной.

Например, Lodash (начиная с версии 4.17.4) добавляет в пакет 72 КБ минимизированного кода. Но если вы используете всего около 20 его методов, то примерно 65 КБ минимизированного кода просто ничего не делают.

Другой пример — Moment.js. Его версия 2.19.1 занимает 223 КБ минимизированного кода, что очень много: в октябре 2017 года средний размер JavaScript на странице составлял 452 КБ . Однако 170 КБ этого размера — это файлы локализации . Если вы не используете Moment.js с несколькими языками, эти файлы будут бесцельно раздувать пакет.

Все эти зависимости можно легко оптимизировать. Мы собрали подходы к оптимизации в репозитории GitHub — посмотрите !

Включить объединение модулей для модулей ES (также известное как подъем объема)

Когда вы создаете пакет, веб-пакет оборачивает каждый модуль в функцию:

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

Раньше это требовалось для изоляции модулей CommonJS/AMD друг от друга. Однако это привело к увеличению размера и производительности каждого модуля.

В Webpack 2 появилась поддержка модулей ES, которые, в отличие от модулей CommonJS и AMD, можно объединять, не оборачивая каждый из них функцией. И Webpack 3 сделал такое объединение возможным — с помощью конкатенации модулей . Вот что делает конкатенация модулей:

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

Видите разницу? В простом пакете модуль 0 требовал render из модуля 1. При объединении модулей require просто заменяется требуемой функцией, а модуль 1 удаляется. В комплекте меньше модулей – и меньше накладных расходов на модули!

Чтобы включить это поведение, в веб-пакете 4 включите опцию optimization.concatenateModules :

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

В веб-пакете 3 используйте ModuleConcatenationPlugin :

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

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

Дальнейшее чтение

Используйте externals , если у вас есть код как веб-пакета, так и код, не относящийся к веб-пакету.

У вас может быть большой проект, в котором часть кода компилируется с помощью веб-пакета, а часть — нет. Как на сайте видеохостинга, где виджет плеера может быть создан с помощью веб-пакета, а окружающая страница — нет:

Скриншот видеохостинга
(Совершенно случайный видеохостинг)

Если обе части кода имеют общие зависимости, вы можете поделиться ими, чтобы не загружать их код несколько раз. Это делается с помощью опции externals веб-пакета — она заменяет модули переменными или другими внешними объектами импорта.

Если зависимости доступны в window

Если ваш код, отличный от веб-пакета, опирается на зависимости, которые доступны как переменные в window , назначьте псевдонимы зависимостей именам переменных:

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

С этой конфигурацией веб-пакет не будет объединять пакеты react и react-dom . Вместо этого они будут заменены чем-то вроде этого:

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

Если зависимости загружаются как пакеты AMD

Если ваш код, отличный от веб-пакета, не раскрывает зависимости в window , все становится сложнее. Однако вы все равно можете избежать двойной загрузки одного и того же кода, если код, отличный от веб-пакета, использует эти зависимости как пакеты AMD .

Для этого скомпилируйте код веб-пакета как пакет AMD и псевдонимы модулей для URL-адресов библиотеки:

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

Webpack обернет пакет в define() и сделает его зависимым от этих URL-адресов:

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

Если код, отличный от веб-пакета, использует одни и те же URL-адреса для загрузки своих зависимостей, то эти файлы будут загружены только один раз — дополнительные запросы будут использовать кеш загрузчика.

Дальнейшее чтение

Подведение итогов

  • Включите производственный режим, если вы используете Webpack 4.
  • Минимизируйте свой код с помощью параметров минификатора и загрузчика на уровне пакета.
  • Удалите код, предназначенный только для разработки, заменив NODE_ENV на production
  • Используйте модули ES, чтобы включить встряхивание деревьев
  • Сжатие изображений
  • Применить оптимизацию для конкретных зависимостей
  • Включить объединение модулей
  • Используйте externals , если это имеет для вас смысл.