美文网首页
5-10~11 webpack 性能优化(2)

5-10~11 webpack 性能优化(2)

作者: love丁酥酥 | 来源:发表于2020-05-24 01:32 被阅读0次

1. 简介

本节主要介绍如何利用 dll 相关技术来提高打包效率。

2. 准备代码

// build/webpack.common.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const path = require('path');

module.exports = {
  entry: {
    index: './src/index.jsx',
  },
  output: {
    path: path.resolve(__dirname, '../dist'),
    filename: '[name].js',
  },
  resolve: {
    extensions: ['.js', '.jsx'],
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node-modules/,
        use: ['babel-loader'],
      },
      {
        test: /\.(jpg|jpeg|png|gif)$/,
        use: {
          loader: 'url-loader',
          options: {
            name: '[name].[ext]',
            limit: 2048,
          },
        },
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
      {
        test: /\.(eot|svg|ttf|woff)$/,
        use: 'file-loader',
      },
    ],
  },
  optimization: {
    runtimeChunk: {
      name: 'runtime',
    },
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          name: 'vendors',
        },
      },
    },
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
      filename: 'index.html',
    }),
    new CleanWebpackPlugin(),
  ],
};
// build/webpack.dev.js
const path = require('path');
const webpack = require('webpack');
const merge = require('webpack-merge');
const commonConfig = require('./webpack.common');

const devConfig = {
  mode: 'development',
  devtool: 'cheap-module-eval-source-map',
  devServer: {
    contentBase: path.resolve(__dirname, 'dist'),
    overlay: true,
    open: true,
    port: 3000,
    hot: true, // 开启热更新
    historyApiFallback: true,
    proxy: {
      '/api': {
        target: 'http://127.0.0.1:3600',
        changeOrigin: true,
      },
    },
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
  ],
};

module.exports = merge(commonConfig, devConfig);
// build/webpack.prod.js
const merge = require('webpack-merge');
const commonConfig = require('./webpack.common');

const prodConfig = {
  mode: 'production',
  devtool: 'cheap-module-source-map',
};

module.exports = merge(commonConfig, prodConfig);
// src/index.jsx
import React, { Component } from 'react';
import ReactDom from 'react-dom';
import { BrowserRouter, Route } from 'react-router-dom';
import Home from './home';
import List from './list';

class App extends Component {
  render() {
    return (
      <BrowserRouter>
        <>
          <Route path="/" exact component={Home} />
          <Route path="/list" component={List} />
        </>
      </BrowserRouter>
    );
  }
}

ReactDom.render(<App />, document.getElementById('root'));
<!--src/index.html-->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>esmodule-oop</title>
</head>
<body>
    <div id="root"></div>
</body>
</html>
// src/home.jsx
import React, { Component } from 'react';

class Home extends Component {
  render() {
    return <div>home</div>;
  }
}

export default Home;
// src/list.jsx
import React, { Component } from 'react';

class List extends Component {
  render() {
    return <div>List</div>;
  }
}

export default List;
  "scripts": {
    "dev": "webpack --config ./build/webpack.dev.js --watch",
    "dev-analyse": "webpack --config ./build/webpack.dev.js --profile --json > stas.json",
    "build-analyse": "webpack --config ./build/webpack.prod.js --profile --json > stas.json",
    "dev-server": "webpack-dev-server --config ./build/webpack.dev.js",
    "build": "webpack --config ./build/webpack.prod.js"
  },

3. 普通打包

首先我们直接使用 dev 命令打包,(这里暂不使用 dev-server )

npm run dev

我们多打几次看看,最终耗时我这边大概是 3500ms 左右。


image.png

然后我们修改一下 home.jsx 代码,增加一个三方库,lodash。我们装一下 lodash 然后引入:

// src/home.jsx
import React, { Component } from 'react';
import _ from 'lodash';

class Home extends Component {
  render() {
    return <div>{_.join(['home', 'page'], ' ')}</div>;
  }
}

export default Home;

然后打几次包,平均耗时大概 3900ms 左右:


image.png

可以看到新增 node_modules 以后,vendors 体积和打包耗时都显著增加了。

4. dll 打包

我们可以看到,node_modules 或造成打包耗时显著增加。可事实上,我们很多 node_modules 都是稳定的,很少更新,每次重新打包 node_modules 并没有意义。如果我们有一个方案,将不变的那些 node_modules 打包一次后存起来,下次打包的时候直接使用已经打包好的这些 node_modules 代码,是不是就会快很多呢?
事实上,webpack 提供了这样的插件来实现上述的功能,就是dll-plugindllreferenceplugin。我们来看一下如何使用:

4.1 将需要抽取的 common 文件单独打包

// build/webpack.dll.js
const path = require('path');

module.exports = {
  mode: 'development', // 这里为了演示,使用 development
  entry: {
    vendors: ['react', 'react-dom', 'lodash'],
  },
  output: {
    filename: '[name].dll.js',
    path: path.resolve(__dirname, '../dll'),
    library: '[name]',
  },
};
"vendors-dll": "webpack --config ./build/webpack.dll.js"

运行上述 vendors-dll 打包命令,生成如下文件:


image.png

4.2 引入打包后的 vendors.dll.js

打包后的文件,还需要让 html 引用。这里我们可以使用 add-asset-html-webpack-plugin插件,来帮我们实现 html 自动引用。

// build/webpack.common.js
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin');
...
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
      filename: 'index.html',
    }),
    new AddAssetHtmlPlugin({ filepath: path.resolve(__dirname, '../dll/vendors.dll.js') }),
    new CleanWebpackPlugin(),
  ],

运行 npm run dev 后,打开 index.html,控制台可以输出 vendors:


image.png
image.png

但是目前我们的打包体积和时间都没有减小。

4.3 dll-plugindllreferenceplugin

dll-plugin 是在一个额外的独立的 webpack 设置中创建一个只有 dll 的 bundle(dll-only-bundle)。 这个插件会生成一个名为 manifest.json 的文件,这个文件是用来让 DLLReferencePlugin 映射到相关的依赖上去的。
DLLReferencePlugin 是在 webpack 主配置文件中设置的, 这个插件把只有 dll 的 bundle(s)(即:dll-only-bundle(s)) 引用到需要的预编译的依赖。
简言之,DllPlugin 和 DLLReferencePlugin 允许用户提前为所有那些不需要关心的 npm 模块创建一个单独的包,教会 Webpack 将它们引用到该包,大大减少了 Webpack 构建包(以及重构包)所需的时间。

// build/webpack.dll.js
const path = require('path');
const webpack = require('webpack');

module.exports = {
  mode: 'development', // 这里为了演示,使用 development
  entry: {
    vendors: ['react', 'react-dom', 'lodash'],
  },
  output: {
    filename: '[name].dll.js',
    path: path.resolve(__dirname, '../dll'),
    library: '[name]',
  },
  plugins: [
    new webpack.DllPlugin({
      path: path.join(__dirname, '../dll', '[name].manifest.json'),
      name: '[name]',
    }),
  ],
};
// build/webpack.common.js
...
plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
      filename: 'index.html',
    }),
    new AddAssetHtmlPlugin({ filepath: path.resolve(__dirname, '../dll/vendors.dll.js') }),
    new webpack.DllReferencePlugin({
      // 注意: DllReferencePlugin 的 context 必须和 package.json 的同级目录,要不然会链接失败
      context: path.resolve(__dirname, '../'),
      manifest: path.resolve(__dirname, '../dll/vendors.manifest.json'),
    }),
    new CleanWebpackPlugin(),
  ],

运行 npm run vendors-dll,然后运行几次 npm run dev,如下:


image.png

时间大幅缩短。我们运行一下 npm run dev-server,看一下页面是否 okay:


image.png
页面运行正常,且打包时间大大缩短

5. 处理多个 dll 文件

并不是只有 node_modules 才可以抽成 dll,一些经常不做变更的 libs 模块也可以。大型项目中,存在很多可以抽离的地方,为了单独更新,也为了控制文件的大小,我们并不会将他们全部放在一个 dll 中。这个时候我们就要多次调用 AddAssetHtmlPlugin 和 DllReferencePlugin,让配置文件显得冗长而且不易管理。这个时候,可以使用 node_modules 动态生成 plugins。

// build/webpack.dll.js
const path = require('path');
const webpack = require('webpack');

module.exports = {
  mode: 'development', // 这里为了演示,使用 development
  entry: {
    react: ['react', 'react-dom'],
    lodash: ['lodash'],
  },
  output: {
    filename: '[name].dll.js',
    path: path.resolve(__dirname, '../dll'),
    library: '[name]',
  },
  plugins: [
    new webpack.DllPlugin({
      path: path.join(__dirname, '../dll', '[name].manifest.json'),
      name: '[name]',
    }),
  ],
};
// build/webpack.common.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin');
const webpack = require('webpack');
const path = require('path');
const fs = require('fs');

const plugins = [
  new HtmlWebpackPlugin({
    template: './src/index.html',
    filename: 'index.html',
  }),
  new CleanWebpackPlugin(),
];

fs.readdirSync(path.resolve(__dirname, '../dll')).forEach(file => {
  if (/.*.dll.js$/.test(file)) {
    plugins.push(new AddAssetHtmlPlugin({
      filepath: path.resolve(__dirname, '../dll', file),
    }));
  }
  if (/.*.manifest.json$/.test(file)) {
    plugins.push(new webpack.DllReferencePlugin({
      manifest: path.resolve(__dirname, '../dll', file),
    }));
  }
});

module.exports = {
  entry: {
    index: './src/index.jsx',
  },
  output: {
    path: path.resolve(__dirname, '../dist'),
    filename: '[name].js',
  },
  resolve: {
    extensions: ['.js', '.jsx'],
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node-modules/,
        use: ['babel-loader'],
      },
      {
        test: /\.(jpg|jpeg|png|gif)$/,
        use: {
          loader: 'url-loader',
          options: {
            name: '[name].[ext]',
            limit: 2048,
          },
        },
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
      {
        test: /\.(eot|svg|ttf|woff)$/,
        use: 'file-loader',
      },
    ],
  },
  optimization: {
    runtimeChunk: {
      name: 'runtime',
    },
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          name: 'vendors',
        },
      },
    },
  },
  plugins,
};

image.png

6. 利用 autodll-webpack-plugin 简化配置

上面我们可以看到,引入 dll 需要非常多的配置,一旦 node_modules 发生变化还需要我们去手动重新编译模块。那么有没有更简单的方式去实现 dll 配置呢?当然是有的,这就是我们要介绍的 autodll-webpack-plugin 插件。
AutoDllPlugin 是 DllPlugin 和 DllReferencePlugin 的高级插件,隐藏了它们的大部分复杂性。

  1. 缓存:当用户第一次构建 bundle 时,AutoDllPlugin 会为您编译 DLL,并将所有指定的模块从 bundle 引用到 DLL。下一次编译代码时,AutoDllPlugin 将跳过构建并从缓存中读取。
  2. 监听变动自动构建:每当更改插件的配置,安装或删除一个 node_module 时,AutoDllPlugin 将重新构建dll。
  3. 内存:当使用Webpack的Dev服务器时,包被加载到内存中,以防止从文件系统进行不必要的读取。
  4. html 自动引入:按照 DLLPlugin 的工作方式,必须先加载 DLL 包,然后再加载自己的包。这通常是通过向 HTML 添加额外的脚本标记来实现的。因为这是一个非常常见的任务,AutoDllPlugin可以为用户完成(与HtmlPlugin一起)。
    下面我们看下如何来使用:
    首先安装该插件,这里要注意 webpack4 和 webpack2/3 安装的版本并不一样。

webpack 4

npm install --save-dev autodll-webpack-plugin

webpack 2 / 3

 npm install --save-dev autodll-webpack-plugin@0.3
// build/webpack.common.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const AutoDllPlugin = require('autodll-webpack-plugin');
const path = require('path');

module.exports = {
  entry: {
    index: './src/index.jsx',
  },
  output: {
    path: path.resolve(__dirname, '../dist'),
    filename: '[name].js',
  },
  resolve: {
    extensions: ['.js', '.jsx'],
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node-modules/,
        use: ['babel-loader'],
      },
      {
        test: /\.(jpg|jpeg|png|gif)$/,
        use: {
          loader: 'url-loader',
          options: {
            name: '[name].[ext]',
            limit: 2048,
          },
        },
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
      {
        test: /\.(eot|svg|ttf|woff)$/,
        use: 'file-loader',
      },
    ],
  },
  optimization: {
    runtimeChunk: {
      name: 'runtime',
    },
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          name: 'vendors',
        },
      },
    },
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
      filename: 'index.html',
    }),
    new AutoDllPlugin({
      inject: true, // will inject the DLL bundle to index.html
      filename: '[name]_[hash].dll.js',
      path: './dll',
      entry: {
        react: [
          'react',
          'react-dom',
        ],
        lodash: [
          'lodash',
        ],
      },
    }),
    new CleanWebpackPlugin(),
  ],
};

npm run dev 打包如下:


image.png

我们不管打包多少遍,可以看到 dll 目录下的文件都是不变的。然后我们试着重新安装 lodash,现在我们用的是 4.17.15,我们切换一下:

npm i lodash@4.17.14 -S

然后 npm run dev


image.png

可以看到 dll 下的文件更新了。

7. 即将被抛弃的 dll

上面可以看到,使用 dll 能极大提升构建速度,可是 dll 本身就是为了弥补 webpack 打包的不足而出现的,随着 webpack 的升级和优化,额外使用插件实现 dll 带来的提升已经越来越小。
在 vue-cli 的某次 pr 中,尤雨溪提到vue-cli/pull/1002 不在维护 dll。并且在一次 commit中删除了对 dll 的支持。

image.png
不过可以看到,vue-cli 本来使用的就是 autodll-webpack-plugin 插件,其可靠性是很有保证的,而且,在 webpack4 来讲,还是有很好效果的。
不过对于 webpack5 来说,这个插件确实就派不上用场了,这不是我说的,这是插件的文档写的。

Now, that webpack 5 planning to support caching out-of-the-box, AutoDllPlugin will soon be obsolete.

In the meantime, I would like to recommend Michael Goddard's hard-source-webpack-plugin, which seems like webpack 5 is going to use internally.

作者明确告诉我们 webpack5 计划支持磁盘缓存,本插件即将废弃。并且向我们推荐了另一款插件,可能会被内置在 webpack5 中。这款插件就是 hard-source-webpack-plugin

8. hard-source-webpack-plugin

HardSourceWebpackPlugin 是 webpack 的一个插件,用于为模块提供中间缓存步骤。为了看到结果,您需要使用这个插件运行两次 webpack: 第一次构建将花费正常的时间。第二个版本的速度将明显加快。
安装:

npm install --save-dev hard-source-webpack-plugin

用法如下:

// build/webpack.common.js
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
 
module.exports = {
  context: // ...
  entry: // ...
  output: // ...
  plugins: [
    new HardSourceWebpackPlugin()
  ]
}
image.png

这速度简直逆天了,配置也超级简单。当然还有更多的配置,可以参考文档。
如果我们修改 node_modules,再打包,发现 vendors 也会自动更新。


image.png

至于文件更新以后,如何清除浏览器缓存的影响,保证代码的更新,只要在 filename 加入 hash 即可:

  output: {
    path: path.resolve(__dirname, '../dist'),
    filename: '[name].[hash].js',
  },

9. 小结

本节其实主要讲的就是如何处理打包过程中的磁盘缓存,并介绍了dll-plugindllreferencepluginautodll-webpack-pluginhard-source-webpack-plugin 等缓存相关的优秀插件。
推荐大家直接使用 hard-source-webpack-plugin 即可。

参考

你是否需要webpack dll
webpack使用-详解DllPlugin
webpack打包指位置Dll打包方式
使用 happypack 提升 Webpack 项目构建速度
Webpack支撑大规模应用开发最佳实践
webpack.DllPlugin和webpack.DllReferencePlugin静态资源预编译插件
辛辛苦苦学会的 webpack dll 配置,可能已经过时了
webapack-doc/dll-plugin
autodll-webpack-plugin

相关文章

网友评论

      本文标题:5-10~11 webpack 性能优化(2)

      本文链接:https://www.haomeiwen.com/subject/hpzlohtx.html