Оптимизация ассетов с помощью webpack

внешний интерфейс JavaScript React.js Webpack
Оптимизация ассетов с помощью webpack

предисловие

При оптимизации интерфейсных приложений чрезвычайно важно контролировать размер загружаемых ресурсов, в большинстве случаев мы можем контролировать размер, разделять и повторно использовать ресурсы в процессе упаковки и компиляции.
Эта статья в основном основана на упаковке веб-пакетов и использует экологически разработанные одностраничные приложения, такие как React и vue, в качестве примера, чтобы проиллюстрировать, как обрабатывать ресурсы и кешировать на уровне упаковки веб-пакета.Главное, что нам нужно сделать, это оптимизировать конфигурация веб-пакета, в то же время вносится небольшое количество изменений в бизнес-код.

В то же время можно использовать анализ упакованных ресурсов.webpack-bundle-analyzerПлагины, конечно, есть еще много необязательных плагинов для анализа.В этой статье этот плагин в основном используется как пример для анализа.

СОВЕТ: версия веб-пакета @3.6.0.


Среда упаковки и сжатие кода

Сначала у нас есть базовая конфигурация веб-пакета:

// webpack.config.js
const path = require('path');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const PROJECT_ROOT = path.resolve(__dirname, './');
module.exports = {
  entry: {
    index: './src/index.js'
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[chunkhash:4].js'
  },
  module: {
    rules: [
      {
        test: /\.js[x]?$/,
        use: 'babel-loader',
        include: PROJECT_ROOT,
        exclude: /node_modules/
      }
    ]
  },
  plugins: [
    new BundleAnalyzerPlugin()
  ],
  resolve: {
    extensions: ['.js', '.jsx']
  },
};

Выполните упаковку, и вы увидите, что js проекта имеет более 1M:

Hash: e51afc2635f08322670b
Version: webpack 3.6.0
Time: 2769ms
        Asset    Size  Chunks                    Chunk Names
index.caa7.js  1.3 MB       0  [emitted]  [big]  index

Просто добавьте плагиныDefinePluginа такжеUglifyJSPluginВы можете уменьшить объем, добавить плагины:

// webpack.config.js
...
{
  ...
  plugins: [
    new BundleAnalyzerPlugin(),
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production')
    }),
    new UglifyJSPlugin({
      uglifyOptions: {
        ie8: false,
        output: {
          comments: false,
          beautify: false,
        },
        mangle: {
          keep_fnames: true
        },
        compress: {
          warnings: false,
          drop_console: true
        },
      }
    })
  ]
  ...
}

Вы можете увидеть вывод упаковки в это время:

Hash: 84338998472a6d3c5c25
Version: webpack 3.6.0
Time: 9940ms
        Asset    Size  Chunks                    Chunk Names
index.89c2.js  346 kB       0  [emitted]  [big]  index

Размер кода был уменьшен с 1.3M до 346K.

DefinePlugin

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

В нашем бизнес-коде и коде сторонних пакетов нам часто приходится судитьprocess.env.NODE_ENVделать разную обработку, а в производстве нам явно не нужны не-productionчасть обработки.
Здесь мы устанавливаемprocess.env.NODE_ENVдляJSON.stringify('production'), что означает, что среда упаковки настроена на производственную среду. сотрудничать позжеUglifyJSPluginПлагины могут удалить часть избыточного кода при упаковке для производства.

UglifyJSPlugin

UglifyJSPlugin в основном используется для разбора и сжатия кода js.uglify-esДля обработки кода js он имеет различные параметры конфигурации:GitHub.com/Webpack-con…
Благодаря сжатию кода и удалению избыточности размер упакованных ресурсов значительно уменьшается.


Разделение кода/загрузка по требованию

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

Однако, когда мы открываем страницу, соответствующую URL-адресу, нам действительно не нужно все код JS, а только основной код выполнения и код бизнес-логика, соответствующий просмотру, а затем загрузить следующий вид. Для загрузки этой части код.
Поэтому оптимизация, которую можно сделать в этом отношении, заключается в загрузке кода js по запросу.

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

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

// webpack.config.js
const path = require('path');
const webpack = require('webpack');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const PROJECT_ROOT = path.resolve(__dirname, './');
module.exports = {
  entry: {
    index: './src/index.js'
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[chunkhash:4].js',
    chunkFilename: '[name].[chunkhash:4].child.js',
  },
  module: {
    rules: [
      {
        test: /\.js[x]?$/,
        use: 'babel-loader',
        include: PROJECT_ROOT,
        exclude: /node_modules/
      }
    ]
  },
  plugins: [
    new BundleAnalyzerPlugin(),
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production')
    }),
    new UglifyJSPlugin({
      uglifyOptions: {
        ie8: false,
        output: {
          comments: false,
          beautify: false,
        },
        mangle: {
          keep_fnames: true
        },
        compress: {
          warnings: false,
          drop_console: true
        },
      }
    }),
  ],
  resolve: {
    extensions: ['.js', '.jsx']
  },
};

Основными из них, которые необходимо определить, являютсяoutputсерединаchunkFilename, которое является именем файла экспортированного кода разделения, здесь установлено значение[name].[chunkhash:4].child.js,один из нихnameСоответствующее имя модуля или идентификатор,chunkhashэто хэш содержимого модуля.

Затем в бизнес-коде webpack предоставляет два способа динамического импорта:

  • import('path/to/module') -> Promise,
  • require.ensure(dependencies: String[], callback: function(require), errorCallback: function(error), chunkName: String)

В основном рекомендуется для последней версии веб-пакетаimport()ПутьПримечание: для импорта используется Promise, поэтому необходимо убедиться, что полифилл Promise поддерживается в коде..

// src/index.js
function getComponent() {
  return import(
    /* webpackChunkName: "lodash" */
    'lodash'
  ).then(_ => {
    var element = document.createElement('div');
    element.innerHTML = _.join(['Hello', 'webpack'], ' ');
    return element;
  }).catch(error => 'An error occurred while loading the component');
}
getComponent().then(component => {
  document.body.appendChild(component);
})

Вы можете увидеть упакованную информацию:

Hash: d6ba79fe5995bcf9fa4d
Version: webpack 3.6.0
Time: 7022ms
               Asset     Size  Chunks             Chunk Names
lodash.89f0.child.js  85.4 kB       0  [emitted]  lodash
       index.316e.js  1.96 kB       1  [emitted]  index
   [0] ./src/index.js 441 bytes {1} [built]
   [2] (webpack)/buildin/global.js 509 bytes {0} [built]
   [3] (webpack)/buildin/module.js 517 bytes {0} [built]
    + 1 hidden module

Вы можете видеть, что упакованный код сгенерированindex.316e.jsа такжеlodash.89f0.child.jsдва файла, последний черезimportдобиться расщепления.
importон получаетpathПараметр относится к пути к подмодулю, а также обратите внимание, что к нему можно добавить строку комментария/* webpackChunkName: "lodash" */, аннотация не бесполезна, она определяет подмодуль название, которое соответствуетoutput.chunkFilenameсередина[name].
importФункция возвращает промис, и при асинхронной загрузке в код подмодуля она будет выполнять последующие операции, такие как обновление представления и т.д.

Загрузка по требованию в React

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

import React, { Component } from 'react';
export default function lazyLoader (importComponent) {
  class AsyncComponent extends Component {
    state = { Component: null }
    async componentDidMount () {
      const { default: Component } = await importComponent();
      this.setState({
        Component: Component
      });
    }
    render () {
      const Component = this.state.Component;
      return Component
        ? <Component {...this.props} />
        : null;
    }
  }
  return AsyncComponent;
};

существуетRouteсередина:

<Switch>
  <Route exact path="/"
    component={lazyLoader(() => import('./Home'))}
  />
  <Route path="/about"
    component={lazyLoader(() => import('./About'))}
  />
  <Route
    component={lazyLoader(() => import('./NotFound'))}
  />
</Switch>

существуетRouteотображается вlazyLoaderКомпонент, возвращаемый функцией, которая будет выполнена после монтированияimportComponentфункция (оба:() => import('./About')) динамически загружает соответствующий модуль компонента (разделенный код) и отображает компонент после успешной загрузки.

Код упакован таким образом:

Hash: 02a053d135a5653de985
Version: webpack 3.6.0
Time: 9399ms
          Asset     Size  Chunks                    Chunk Names
0.db22.child.js  5.82 kB       0  [emitted]
1.fcf5.child.js   4.4 kB       1  [emitted]
2.442d.child.js     3 kB       2  [emitted]
  index.1bbc.js   339 kB       3  [emitted]  [big]  index

Извлечь общие ресурсы

Длинный кеш для сторонних библиотек

Во-первых, для некоторых относительно больших сторонних библиотек, таких как react, react-dom, react-router и т.п., используемых в React, мы не хотим, чтобы они упаковывались повторно, и не хотим каждый раз менять версия обновлена ​​Эта часть ресурса вызывает перезагрузку на стороне пользователя.
Здесь вы можете использовать CommonsChunkPlugin webpack для извлечения этих общих ресурсов;

Плагин CommonsChunkPlugin — это дополнительная функция для создания отдельного файла (также называемого чанком), который содержит общие модули для чанков с несколькими входами. Убрав общие модули, окончательный синтезированный файл можно загрузить один раз в начале, а затем сохранить в кэше для последующего использования. Это дает прирост скорости, потому что браузер быстро извлекает общий код из кеша, а не загружает файл большего размера каждый раз, когда открывается новая страница.

Во-первых, вам нужно добавить запись в запись для упаковки библиотеки, которую необходимо извлечь.Здесь мы будем'react', 'react-dom', 'react-router-dom', 'immutable'упакованы индивидуальноvendorсередина;
Затем определите плагин в плагинахCommonsChunkPluginплагин, покаnameУстановить какvendorв том, что они связаны, а затемminChunksУстановить какInfinityПредотвратить упаковку другого кода.

// webpack.config.js
const path = require('path');
const webpack = require('webpack');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const PROJECT_ROOT = path.resolve(__dirname, './');
module.exports = {
  entry: {
    index: './src0/index.js',
    vendor: ['react', 'react-dom', 'react-router-dom', 'immutable']
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[chunkhash:4].js',
    chunkFilename: '[name].[chunkhash:4].child.js',
  },
  module: {
    rules: [
      {
        test: /\.js[x]?$/,
        use: 'babel-loader',
        include: PROJECT_ROOT,
        exclude: /node_modules/
      }
    ]
  },
  plugins: [
    new BundleAnalyzerPlugin(),
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production')
    }),
    new UglifyJSPlugin({
      uglifyOptions: {
        ie8: false,
        output: {
          comments: false,
          beautify: false,
        },
        mangle: {
          keep_fnames: true
        },
        compress: {
          warnings: false,
          drop_console: true
        },
      }
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: Infinity,
    }),
  ],
  resolve: {
    extensions: ['.js', '.jsx']
  },
};

Вы можете увидеть пакет:

Hash: 34a71fcfd9a24e810c21
Version: webpack 3.6.0
Time: 9618ms
          Asset     Size  Chunks                    Chunk Names
0.2c65.child.js  5.82 kB       0  [emitted]
1.6e26.child.js   4.4 kB       1  [emitted]
2.e4bc.child.js     3 kB       2  [emitted]
  index.4e2f.js  64.2 kB       3  [emitted]         index
 vendor.5fd1.js   276 kB       4  [emitted]  [big]  vendor

можно увидетьvendorупакован отдельно.

Упакуйте снова, когда мы изменим бизнес-код:

Hash: cd3f1bc16b28ac97e20a
Version: webpack 3.6.0
Time: 9750ms
          Asset     Size  Chunks                    Chunk Names
0.2c65.child.js  5.82 kB       0  [emitted]
1.6e26.child.js   4.4 kB       1  [emitted]
2.e4bc.child.js     3 kB       2  [emitted]
  index.4d45.js  64.2 kB       3  [emitted]         index
 vendor.bc85.js   276 kB       4  [emitted]  [big]  vendor

Пакет вендора тоже запакован, но у него изменился хэш файла, что явно не соответствует нашим требованиям к длинному кешу.
Это связано с тем, что webpack будет генерировать код среды выполнения при использовании CommoChunkPlugin (он в основном используется для сопоставления взаимосвязей модулей кода), и даже если код поставщика не изменен, среда выполнения все равно будет следовать изменениям упаковки и вводить verdor , поэтому хэш начнет меняться. Решение состоит в том, чтобы извлечь эту часть кода среды выполнения отдельно и изменить предыдущую.CommonsChunkPluginдля:

// webpack.config.js
...
new webpack.optimize.CommonsChunkPlugin({
  name: ['vendor', 'runtime'],
  minChunks: Infinity,
}),
...

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

Конечно этоruntimeНа самом деле он очень короткий, мы можем напрямую встроить его в html, если используемhtml-webpack-pluginПлагин обрабатывает html, можно комбинироватьhtml-webpack-inline-source-pluginПлагин обрабатывает свои в линию.

абстракция государственных ресурсов

Упакованные нами js-ресурсы включают в себя наборы js-ресурсов из разных записей и подмодулей, но одни и те же зависимые модули или код будут многократно загружаться между ними, поэтому некоторые ресурсы, от которых они зависят вместе, можно упаковать в общий с помощью плагина CommonsChunkPlugin. Ресурсы.

// webpack.config.js
plugins: [
  new BundleAnalyzerPlugin(),
  new webpack.DefinePlugin({
    'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production')
  }),
  new UglifyJSPlugin({
    uglifyOptions: {
      ie8: false,
      output: {
        comments: false,
        beautify: false,
      },
      mangle: {
        keep_fnames: true
      },
      compress: {
        warnings: false,
        drop_console: true
      },
    }
  }),
  new webpack.optimize.CommonsChunkPlugin({
    name: ['vendor', 'runtime'],
    minChunks: Infinity,
  }),
  new webpack.optimize.CommonsChunkPlugin({
    // ( 公共chunk(commnons chunk) 的名称)
    name: "commons",
    // ( 公共chunk 的文件名)
    filename: "commons.[chunkhash:4].js",
    // (模块必须被 3个 入口chunk 共享)
    minChunks: 3
  })
],

Вы можете видеть, что он был добавлен здесьcommonsКогда ресурс зависит от трех или более фрагментов, эти ресурсы будут отдельно извлечены и упакованы вcommons.[chunkhash:4].jsдокумент.

Выполните пакет и увидите результат следующим образом:

Hash: 2577e42dc5d8b94114c8
Version: webpack 3.6.0
Time: 24009ms
          Asset     Size  Chunks                    Chunk Names
0.2eee.child.js  90.8 kB       0  [emitted]
1.cfbc.child.js  89.4 kB       1  [emitted]
2.557a.child.js    88 kB       2  [emitted]
 vendor.66fd.js   275 kB       3  [emitted]  [big]  vendor
  index.688b.js  64.2 kB       4  [emitted]         index
commons.a61e.js  1.78 kB       5  [emitted]         commons

но нашел здесьcommons.[chunkhash].jsПо сути, фактического контента нет, но очевидно, что каждый подмодуль также зависит от одних и тех же зависимостей.
с помощьюwebpack-bundle-analyzerПроанализируем волну:

img

Вы можете видеть, что все три модуля зависят отlodash, однако он не извлекается.

Это связано с тем, что чанк в CommonsChunkPlugin ссылается на каждую запись в записи, поэтому он не влияет на дочерний фрагмент, отделенный от записи.
Можно настроить в плагине CommonsChunkPluginchildrenПараметр также упаковывает общедоступные зависимости разделенных подмодулей вcommonsсередина:

// webpack.config.js
plugins: [
  new BundleAnalyzerPlugin(),
  new webpack.DefinePlugin({
    'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production')
  }),
  new UglifyJSPlugin({
    uglifyOptions: {
      ie8: false,
      output: {
        comments: false,
        beautify: false,
      },
      mangle: {
        keep_fnames: true
      },
      compress: {
        warnings: false,
        drop_console: true
      },
    }
  }),
  new webpack.optimize.CommonsChunkPlugin({
    name: ['vendor', 'runtime'],
    minChunks: Infinity,
  }),
  new webpack.optimize.CommonsChunkPlugin({
    // ( 公共chunk(commnons chunk) 的名称)
    name: "commons",
    // ( 公共chunk 的文件名)
    filename: "commons.[chunkhash:4].js",
    // (模块必须被 3个 入口chunk 共享)
    minChunks: 3
  }),
  new webpack.optimize.CommonsChunkPlugin({
    // (选择所有被选 chunks 的子 chunks)
    children: true,
    // (在提取之前需要至少三个子 chunk 共享这个模块)
    minChunks: 3,
  })
],

Проверьте эффект упаковки:

img

Публичные ресурсы его подмодулей упакованы вindex, и не идеально упакованы вcommonsсреди них, или потому чтоcommonsДля входного модуля на входе, и нет трех модулей входа, разделяющих ресурсы;
Возможность удаления в однократных заявкахcommons, в то время как в подмодулеCommonsChunkPluginконфиг в конфигеasyncдляtrue:

// webpack.config.js
plugins: [
  new BundleAnalyzerPlugin(),
  new webpack.DefinePlugin({
    'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production')
  }),
  new UglifyJSPlugin({
    uglifyOptions: {
      ie8: false,
      output: {
        comments: false,
        beautify: false,
      },
      mangle: {
        keep_fnames: true
      },
      compress: {
        warnings: false,
        drop_console: true
      },
    }
  }),
  new webpack.optimize.CommonsChunkPlugin({
    name: ['vendor', 'runtime'],
    minChunks: Infinity,
  }),
  new webpack.optimize.CommonsChunkPlugin({
    // (选择所有被选 chunks 的子 chunks)
    children: true,
    // (异步加载)
    async: true,
    // (在提取之前需要至少三个子 chunk 共享这个模块)
    minChunks: 3,
  })
],

Проверьте эффект:

img

Общие ресурсы подмодулей упакованы в0.9c90.child.jsВ, модуль является общим достоянием подмодуля.


tree shaking

Встряхивание дерева — это термин, обычно используемый для описания удаления мертвого кода из контекста JavaScript. Он основан на статических структурных функциях модульной системы ES2015, таких как импорт и экспорт. Этот термин и концепция фактически возникли из набора инструментов для упаковки модулей ES2015.

Когда мы вводим определенный вывод зависимости, нам может понадобиться только определенная часть кода зависимости, а другая часть кодаunusedДа, если эту часть кода можно удалить, конечный объем упакованного ресурса также может быть значительно уменьшен.
Во-первых, реализация встряски дерева в webpack основана на механизме модуля es2015, поддерживаемом внутри webpack.Большую часть времени мы используем babel для компиляции js-кода, и babel будет обрабатывать его через собственный механизм загрузки модулей, что приводит к встряхивание дерева в веб-пакете Обработка не удастся. Поэтому нужно отключить обработку загрузки модуля в конфигурации babel:

// .babelrc
{
  "presets": [
    [
      "env", {
        "modules": false,
      }
    ],
    "stage-0"
  ],
  ...
}

Тогда давайте посмотрим, как webpack обрабатывает запакованный код, например, есть входной файлindex.jsс однимutils.jsдокумент:

// utils.js
export function square(x) {
  return x * x;
}
export function cube(x) {
  return x * x * x;
}
// index.js
import { cube } from './utils.js';
console.log(cube(10));

Упакованный код:

// index.bundle.js
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
/* unused harmony export square */
/* harmony export (immutable) */ __webpack_exports__["a"] = cube;
function square(x) {
  return x * x;
}
function cube(x) {
  return x * x * x;
}
Видно, что толькоcubeфункция__webpack_exports__экспорт иsquareфункция отмечена какunused harmony export square, однако в упакованном коде обаsquareне экспортируется, но он все еще существует в коде, и как удалить его код можно сделать, добавивUglifyjsWebpackPluginплагин для обработки.