[Перевод] Google — Использование веб-пакета для оптимизации веб-производительности (1): уменьшение размера внешних ресурсов

внешний интерфейс JavaScript SVG Webpack
[Перевод] Google — Использование веб-пакета для оптимизации веб-производительности (1): уменьшение размера внешних ресурсов

представлять

Современные веб-приложения часто используютупаковочный инструментдля создания производственных «упакованных» файлов (скриптов, стилей и т. д.), которые проходят черезоптимизацияа такжекомпрессияПосле этого он может быть загружен пользователями очень быстро. существуетоптимизация веб-производительности с помощью веб-пакетаВ этой серии статей мы расскажем, как использовать webpack для эффективной оптимизации ресурсов сайта. Это поможет пользователям быстрее загружать сайт и взаимодействовать с ним.

webpack logo

webpack — один из самых популярных инструментов для создания пакетов. Мы можем воспользоваться его возможностями для оптимизации кода,разделение кодаСкрипты можно разделить на основные и второстепенные части, а бесполезный код удалить (это всего лишь небольшой пример оптимизации), гарантируя, что ваше приложение будет иметь минимальные накладные расходы на сеть и затраты на обработку.

Before and after applying JavaScript   optimizations. Time-to-Interactive is improved

Сьюзи ЛуРазделение кода в Bundle Buddyвдохновение.

⭐️ Примечание:Мы создали приложение для упражнений, чтобы продемонстрировать, о чем идет речь в этой статье. Постарайтесь максимально использовать эти навыки:webpack-training-project

Начнем оптимизацию с JavaScript, одного из самых ресурсоемких приложений в современных приложениях.

Часть 1: Уменьшение размера передних ресурсов

Когда вы оптимизируете приложение, первое, что нужно сделать, это максимально уменьшить его размер. Вот как это сделать с помощью webpack.

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

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

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

При создании приложения для производства убедитесь, что вы включилиproductionмодель. Это заставит веб-пакет включить свои оптимизации, такие как: уменьшение размера, удаление кода в библиотеке, который доступен только в режиме разработчика.так далее.

Расширенное чтение

включить свернуть

⭐️Уведомление:Большинство из них работают только с webpack 3. если тыРежим производства включен в веб-пакете 4, минимизация на уровне пакета уже включена — вам просто нужно включитьпараметры загрузчика.

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

// 原来的代码
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;
}

// 最小化后的代码
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. Минификатор будет примерно сжиматься следующим образом:

    // 最小化过的 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)
    

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

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

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

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

⭐️Уведомление:В веб-пакете 3 плагин UglifyJS не может компилировать код старше ES2015 (т.е. ES6). Это означает, что если ваш код использует классы, стрелочные функции или другие новые возможности языка, вы не можете скомпилировать их в код ES5, иначе плагин выдаст ошибку.

Если вам нужно скомпилировать (код), который содержит новый синтаксис, используйтеuglifyjs-webpack-pluginплагин. Это также плагин, который поставляется с веб-пакетом, но версия обновлена ​​и может компилировать код ES2015+.

параметры загрузчика

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

/* comments.css */  
.comment {  
  color: black;  
}  

// 最小化后的 bundle.js (部分代码)
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

⭐️Уведомление:Это работает только с веб-пакетом 3. если тыИспользуйте webpack 4 в рабочем режиме,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 ведет себя аналогично — загружает dev-сборку с предупреждением:

// 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 (基于 webpack 4)
module.exports = {
  optimization: {
    nodeEnv: 'production',
    minimize: true,
  },
}; 

В веб-пакете 3,затем используйтеDefinePluginзаменить:

// webpack.config.js (基于 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. потомminifierУдалит все как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, веб-пакет может выполнять встряску деревьев. Tree-shaking — это когда сборщик обходит все дерево зависимостей, проверяет, какие зависимости используются, и удаляет бесполезные. Итак, если вы используете синтаксис модуля ES, webpack может удалить неиспользуемый код:

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

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

    // bundle.js (和 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. minifierУдалить неиспользуемые переменные:

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

Это справедливо даже для библиотек, написанных с модулями ES.

⭐️Уведомление:В вебпаке тряска деревьев не будет работать без минификатора. Webpack удаляет только неиспользуемые переменные экспорта, а минификатор удаляет неиспользуемый код. Так что, если собрать бандл без минификатора, то сокращения не будет.

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

предупреждать:Не компилируйте модули ES в модули CommonJS.

Если вы используете Babelbabel-preset-envилиbabel-preset-es2015, проверьте их предустановленные настройки. По умолчанию они будут использовать ESimportа такжеexportПеренесено в CommonJSrequireа такжеmodule.exports.пройти через{ modules: false }Опцииотключить его.

То же самое с TypeScript: не забудьте добавитьtsconfig.jsonустановить в{ "compilerOptions": { "module": "es2015" } }.

Расширенное чтение

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

Изображение занимает размер страницыбольше половины. Хотя они не так критичны, как 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: {
          // 小于 10kB(10240字节)的内联文件
          limit: 10 * 1024,
        },
      },
    ],
  }
};
// index.js
import imageUrl from './image.png';
// → 如果图片小于 10kB, `imageUrl` 将包含
// 编码后的图片: 'data:image/png;base64,iVBORw0KGg…'
// → 如果图片大于 10B,该 loader 将创建一个新文件,
// 并且 `imageUrl` 将会包含它的 url: `/2fcd56a1920be.png`

⭐️Уведомление:Встроенные изображения уменьшают количество отдельных запросов, что хорошо (даже через HTTP/2), но увеличивает время загрузки/анализа пакетов и потребление памяти. Убедитесь, что вы не встраиваете большие или много изображений, иначе добавленное время пакета может перевесить преимущества встраивания.

svg-url-loaderработает какurl-loader- за исключением того, что он используетURL encodingвместо Base64 для кодирования файла. Это работает для изображений SVG — поскольку файлы SVG представляют собой обычный текст, этот эффект масштабирования кодирования еще более выражен:

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.svg$/,
        loader: 'svg-url-loader',
        options: {
          // 小于 10kB(10240字节)的内联文件
          limit: 10 * 1024,
          // 移除 url 中的引号
          // (在大多数情况下它们都不是必要的)
          noquotes: true,
        },
      },
    ],
  },
};

⭐️Уведомление:В svg-url-loader есть опции для улучшения поддержки IE, но в других браузерах он хуже. Если вам нужна совместимость с браузером IE,настраиватьiesafe: 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',
        // 这会应用该 loader,在其它之前
        enforce: 'pre',
      },
    ],
  },
};

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

Расширенное чтение

зависимости оптимизации

В среднем более половины размера Javascript приходится на зависимости, и некоторые из них могут не понадобиться.

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

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

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

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

⭐️Уведомление:При использовании в производственном режимеwebpack 4, тандем модулей включен. Не стесняйтесь пропустить этот раздел.

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

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

// 与前面的代码段不同,此包只有一个模块
// 它包含来自两个文件的代码

// bundle.js (部分; 通过 ModuleConcatenationPlugin 编译)
/* 0 */
(function(module, __webpack_exports__, __webpack_require__) {

  "use strict";
  Object.defineProperty(__webpack_exports__, "__esModule", { value: true });

  // 级联模块: ./comments.js
  function render(data, target) {
    console.log('Rendered!');
  }

  // 级联模块: ./index.js
  render();

})

Увидеть разницу? В обычном связывании модуль 0 нуждается в модулях 1.render. Подключить с помощью модулей,requireМодуль 1 удаляется простой заменой на нужную функцию. Пакеты имеют меньшие модули - и меньше накладных расходов на модули!

быть вwebpack 4включить эту функцию вoptimization.concatenateModulesВарианты:

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

существуетwebpack 3в использованииModuleConcatenationPluginПлагин:

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

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

⭐️Уведомление:Хотите знать, почему это поведение не включено по умолчанию? Модуль подключения отличный,Но это увеличивает время сборки и разрывает модуль горячей замены. Вот почему он включен только в производственной среде.

Расширенное чтение

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

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

视频托管网站屏幕快照

(Абсолютно случайный видеохостинг)

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

если зависит отwindowдоступно в

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

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

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

// bundle.js (part of)
(function(module, exports) {
  // 导出 `window.React` 的模块。 没有 `externals`,
  // 这个模块会包含整个的 React 包
  module.exports = React;
}),
(function(module, exports) {
  // 导出 `window.React` 的模块。 没有 `externals`,
  // 这个模块会包含整个的 ReactDOM 包
  module.exports = ReactDOM;
})

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

Если ваш код, отличный от webpack, не предоставляет зависимостей для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 (开始)
define(["/libraries/react.min.js", "/libraries/react-dom.min.js"], function () { … });

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

⭐️Уведомление:Webpack заменяет только те, которые явно соответствуютexternalsИмпорт ключа объекта. Это означает, что если вы напишетеimport React from 'react/umd/react.production.min.js', эта библиотека не будет исключена из комплекта. Это разумно - webpack не знаетimport 'react'а такжеimport 'react/umd/react.production.min.js'Это одно и то же - так что будьте осторожны.

Расширенное чтение

  • документация веб-пакетаexternals

Суммировать

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

Чтобы узнать больше, подпишитесь на YFE:

Следующее сообщение: Google — использование веб-пакета для оптимизации веб-производительности (2): эффективное использование постоянного кэширования