ホームページ  >  記事  >  ウェブフロントエンド  >  Webpack を使用してリソースのメソッドとテクニックを最適化する方法

Webpack を使用してリソースのメソッドとテクニックを最適化する方法

小云云
小云云オリジナル
2018-01-04 14:16:371599ブラウズ

フロントエンド アプリケーションの最適化では、ロードされるリソースのサイズを制御することが非常に重要です。ほとんどの場合、できることは、パッケージ化とコンパイルのプロセス中にサイズを制御し、リソースを分割して再利用することです。この記事では、主に webpack を使用してリソースを最適化する方法を紹介します。編集者がそれを共有し、参考にします。編集者をフォローして見てみましょう。皆さんのお役に立てれば幸いです。

前書き

この記事は主に Webpack パッケージ化に基づいており、Webpack パッケージ化レベルからリソースとキャッシュを処理する方法を例として使用しています。やるべきことは、Webpack を行うことです。ビジネス コードを少量変更しながら、構成の最適化を実行します。

同時に、(webpack-contrib/webpack-bundle-analyzer) プラグインを使用して、パッケージ化されたリソースを分析することもできます。この記事では、このプラグインを使用します。主に例として使用されます。

ヒント: webpack バージョン @3.6.0

1. パッケージ化環境とコード圧縮

まず、基本的な webpack 構成があります:

// 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 に縮小されました。

(1)DefinePlugin

DefinePlugin を使用すると、コンパイル時に構成できるグローバル定数を作成できます。これは、開発モードとリリース モードのビルドで異なる動作を許可するのに役立つ場合があります。リリース ビルドではなく開発ビルドでログ記録を実行している場合は、グローバル定数を使用してログを記録するかどうかを決定できます。ここで DefinePlugin が登場します。これをセットアップすれば、開発およびリリース ビルドのルールは忘れて済みます。


ビジネスコードやサードパーティのパッケージコードでは、異なる処理を行うために`process.env.NODE_ENV`を判断する必要があることがよくありますが、本番環境では明らかに非`本番`の処理部分は必要ありません。

ここでは `process.env.NODE_ENV` を `JSON.stringify('production')` に設定します。これは、パッケージ化環境が運用環境に設定されることを意味します。次に、「UglifyJSPlugin」プラグインを使用すると、運用環境用にパッケージ化するときに冗長なコードを削除できます。

(2) UglifyJSPlugin

UglifyJSPlugin は主に js コードの解析と圧縮に使用され、js コードを処理するための `uglify-es` に基づいて、さまざまな設定オプションがあります: https://github.com/webpack - contrib/uglifyjs-webpack-plugin。

コードを圧縮し、冗長性を削除することで、パッケージ化されたリソースのサイズが大幅に削減されます。

2. コード分割/オンデマンド読み込み

React または Vue で構築された単一ページ アプリケーションでは、ページのルーティングとビューの制御はフロントエンドによって実装され、それに対応するビジネス ロジックは JS コード内にあります。

アプリケーション設計に多くのページとロジックがある場合、最終的に生成される js ファイル リソースも非常に大きくなります。

ただし、URL に対応するページを開くときに、実際にはすべての JS コードは必要ありません。必要なのは、メインのランタイム コードと、次のビューを読み込むときに必要なビジネス ロジック コードだけです。コードのその部分をロードします。

したがって、この点に関して実行できる最適化は、オンデマンドで JS コードをロードすることです。

遅延読み込みまたはオンデマンド読み込みは、Web ページまたはアプリケーションを最適化する良い方法です。このメソッドは、実際にはいくつかの論理ブレークポイントでコードを分離し、いくつかのコード ブロックで特定の操作を完了した後、すぐに他の新しいコード ブロックを参照するか、参照しようとしています。これにより、特定のコード ブロックが読み込まれない可能性があるため、アプリの初期読み込みが高速化され、アプリ全体のサイズが削減されます。


Webpack は、コード分割を実現するための動的インポート技術を提供します。 まず、Webpack の設定で、分割された各サブモジュールの設定を行う必要があります:

// 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` はモジュール名または ID に対応します。 ` はモジュールコンテンツのハッシュです。

その後、webpack はビジネス コードに動的にインポートする 2 つの方法を提供します:

  • `import('path/to/module') -> Promise`、

  • `require.ensure(dependency: String [] 、callback: function(require)、errorCallback: function(error)、chunkName: String)`
  • 最新バージョンの webpack では、主に `import()` を使用することをお勧めします (注: import は Promise を使用するため、 Promise ポリフィルがコードでサポートされていることを確認する必要があります)。
// src/index.js
function getComponent() {
 return import(
  /* webpackChunkName: "lodash" */
  'lodash'
 ).then(_ => {
  var element = document.createElement('p');

  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

パッケージ化されたコードが 2 つのファイル、`index.316e.js` と `lodash.89f0.child.js` を生成することがわかります。後者は `import ` を介して渡されます。分割を実現します。

`import` 它接收一个 `path` 参数,指的是该子模块对于的路径,同时还注意到其中可以添加一行注释 `/* webpackChunkName: "lodash" */`,该注释并非是无用的,它定义了该子模块的 name,其对应与 `output.chunkFilename` 中的 `[name]`。

`import` 函数返回一个 Promise,当异步加载到子模块代码是会执行后续操作,比如更新视图等。

(1)React 中的按需加载

在 React 配合 React-Router 开发中,往往就需要代码根据路由按需加载的能力,下面是一个基于 webpack 代码动态导入技术实现的 React 动态载入的组件:

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` 函数返回的组件,该组件在 mount 之后会去执行 `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

三、抽离 Common 资源

(1)第三方库的长缓存

首先对于一些比较大的第三方库,比如在 React 中用到的 react、react-dom、react-router 等等,我们不希望它们被重复打包,并且在每次版本更新的时候也不希望去改变这部分的资源导致在用户端重新加载。

在这里可以使用 webpack 的 CommonsChunkPlugin 来抽离这些公共资源;

CommonsChunkPlugin 插件,是一个可选的用于建立一个独立文件(又称作 chunk)的功能,这个文件包括多个入口 chunk 的公共模块。通过将公共模块拆出来,最终合成的文件能够在最开始的时候加载一次,便存起来到缓存中供后续使用。这个带来速度上的提升,因为浏览器会迅速将公共的代码从缓存中取出来,而不是每次访问一个新页面时,再去加载一个更大的文件。
首先需要在 entry 中新增一个入口用来打包需要抽离出来的库,这里将 `'react', 'react-dom', 'react-router-dom', 'immutable'` 都给单独打包进 `vendor` 中;

之后在 plugins 中定义一个 `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

vendor 包同样被打包出来的,然而它的文件 hash 却发生了变化,这显然不符合我们长缓存的需求。

这是因为 webpack 在使用 CommoChunkPlugin 的时候会生成一段 runtime 代码(它主要用来处理代码模块的映射关系),而哪怕没有改变 vendor 里的代码,这个 runtime 仍然是会跟随着打包变化的并且打入 verdor 中,所以 hash 就会开始变化了。解决方案则是把这部分的 runtime 代码也单独抽离出来,修改之前的 `CommonsChunkPlugin` 为:

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

执行打包可以看到生成的代码中多了 `runtime` 文件,同时即使改变业务代码,vendor 的 hash 值也保持不变了。

当然这段 `runtime` 实际上非常短,我们可以直接 inline 在 html 中,如果使用的是 `html-webpack-plugin` 插件处理 html,则可以结合 [`html-webpack-inline-source-plugin`](DustinJackson/html-webpack-inline-source-plugin) 插件自动处理其 inline。

(2)公共资源抽离

在我们打包出来的 js 资源包括不同入口以及子模块的 js 资源包,然而它们之间也会重复载入相同的依赖模块或者代码,因此可以通过 CommonsChunkPlugin 插件将它们共同依赖的一些资源打包成一个公共的 js 资源。

// 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` 的一个打包,当一个资源被三个以及以上 chunk 依赖时,这些资源会被单独抽离打包到 `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 来分析一波:

可以看到三个模块都依赖了 `lodash`,然而它并没有被抽离出来。

这是因为 CommonsChunkPlugin 中的 chunk 指的是 entry 中的每个入口,因此对于一个入口拆分出来的子模块(children chunk)是不生效的。

可以通过在 CommonsChunkPlugin 插件中配置 `children` 参数将拆分出来的子模块的公共依赖也打包进 `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,
 })
],

查看打包效果:

其子模块的公共资源都被打包到 `index` 之中了,并没有理想地打包进 `commons` 之中,还是因为 `commons` 对于的是 entry 中的入口模块,而这里并未有 3 个 entry 模块共用资源;

在单入口的应用中可以选择去除 `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,
 })
],

查看效果:

子模块的公共资源都被打包到 `0.9c90.child.js` 中了,该模块则是子模块的 commons。

四、tree shaking

tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块系统中的静态结构特性,例如 import 和 export。这个术语和概念实际上是兴起于 ES2015 模块打包工具 rollup。
在我们引入一个依赖的某个输出的时候,我们可能需要的仅仅是该依赖的某一部分代码,而另一部分代码则是 `unused` 的,如果能够去除这部分代码,那么最终打包出来的资源体积也是可以有可观的减小。

首先,webpack 中实现 tree shaking 是基于 webpack 内部支持的 es2015 的模块机制,在大部分时候我们使用 babel 来编译 js 代码,而 babel 会通过自己的模块加载机制处理一遍,这导致 webpack 中的 tree shaking 处理将会失效。因此在 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;
}
"

"js
// 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` 插件来处理。

相关推荐:

实例详解Webpack优化配置缩小文件搜索范围

webpack性能优化

webpack配置之后端渲染详解

以上がWebpack を使用してリソースのメソッドとテクニックを最適化する方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。