Webpack优化构建速度

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Webpack优化构建速度相关的知识,希望对你有一定的参考价值。

参考技术A 1. 使用高版本的 Webpack 和 Node.js

2. 压缩代码

1). 通过 uglifyjs-webpack-plugin 压缩JS代码

2). 通过 mini-css-extract-plugin 提取 chunk 中的 CSS 代码到单独文件,通过 css-loader 的 minimize 选项开启 cssnano 压缩 CSS。

3. 多线程/多进程构建:thread-loader, HappyPack

4. 压缩图片: image-webpack-loader

5. 缩小打包作用域

1). exclude/include (确定 loader 规则范围)

2). resolve.modules 指明第三方模块的绝对路径 (减少不必要的查找)

3). resolve.mainFields 只采用 main 字段作为入口文件描述字段 (减少搜索步骤,需要考虑到所有运行时依赖的第三方模块的入口文件描述字段)

4). resolve.extensions 尽可能减少后缀尝试的可能性

5). noParse 对完全不需要解析的库进行忽略 (不去解析但仍会打包到 bundle 中,注意被忽略掉的文件里不应该包含 import、require、define 等模块化语句)

6). ignorePlugin (完全排除模块)

7). 合理使用alias

6. 提取页面公共资源, 基础包分离

1). 使用 html-webpack-externals-plugin,将基础包通过 CDN 引入,不打入 bundle 中。

2). 使用 SplitChunksPlugin 进行(公共脚本、基础包、页面公共文件)分离(Webpack4内置) ,替代了 CommonsChunkPlugin 插件。

7. 充分利用缓存提升二次构建速度:

babel-loader 开启缓存

terser-webpack-plugin 开启缓存

使用 cache-loader 或者 hard-source-webpack-plugin

8. Tree shaking

打包过程中检测工程中没有引用过的模块并进行标记,在资源压缩时将它们从最终的bundle中去掉(只能对ES6 Modlue生效) 开发中尽可能使用ES6 Module的模块,提高tree shaking效率

禁用 babel-loader 的模块依赖解析,否则 Webpack 接收到的就都是转换过的 CommonJS 形式的模块,无法进行 tree-shaking

使用 PurifyCSS(不在维护) 或者 uncss 去除无用 CSS 代码

purgecss-webpack-plugin 和 mini-css-extract-plugin配合使用(建议)

9. Scope hoisting

构建后的代码会存在大量闭包,造成体积增大,运行代码时创建的函数作用域变多,内存开销变大。Scope hoisting 将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当地重命名一些变量以防止变量名冲突。

前端ES6高频面试题

前端高频面试题Web移动端

web前端AJAX高频面试题

web前端JS基础高频面试题

web前端CSS高频面试题

性能优化:Webpack 优化构建速度

Webpack 优化构建速度

3.1、 缩小文件搜索范围

搜索过程优化包括:

::: tip

  • 优化 resolve.modules 配置
  • 优化 resolve.noParse 配置
  • 优化 resolve.extensions 配置
  • 优化 resolve.noParse 配置
  • 优化 优化 Loader 配置

:::

  1. 优化 resolve.modules 配置

    resolve.modules 用于配置 Webpack 去哪些目录下寻找第三方模块。resolve.modules 的默认值是[node modules],含义是先去当前目录的/node modules 目录下去找我们想找的模块,如果没找到,就去上一级目录../node modules 中找,再没有就去../ .. /node modules 中找,以此类推,这和 Node.js 的模块寻找机制很相似。当安装的第三方模块都放在项目根目录的./node modules 目录下时,就没有必要按照默认的方式去一层层地寻找,可以指明存放第三方模块的绝对路径,以减少寻找。

优化后的配置

resolve: {
// 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤
modules: [path.resolve(__dirname,'node_modules')]
},
  • 设置 resolve.mainFields:['main'],设置尽量少的值可以减少入口文件的搜索步骤.

    第三方模块为了适应不同的使用环境,会定义多个入口文件,mainFields 定义使用第三方模块的哪个入口文件,由于大多数第三方模块都使用 main 字段描述入口文件的位置,所以可以设置单独一个 main 值,减少搜索

  1. 优化 resolve.alias 配置

对庞大的第三方模块设置 resolve.alias, 使 webpack 直接使用库的 min 文件,避免库内解析。

 alias: {
  '@': resolve('src'),
},
// 通过以上的配置,引用src底下的common.js文件,就可以直接这么写
import common from '@/common.js';

这样会影响 Tree-Shaking,适合对整体性比较强的库使用,如果是像 lodash 这类工具类的比较分散的库,比较适合 Tree-Shaking,避免使用这种方式。

  1. 优化 resolve.extensions

合理配置 resolve.extensions,减少文件查找默认值:extensions:['.js', '.json'],当导入语句没带文件后缀时,Webpack 会根据 extensions 定义的后缀列表进行文件查找,所以:

  • 列表值尽量少
  • 频率高的文件类型的后缀写在前面
  • 源码中的导入语句尽可能的写上文件后缀,如 require(./data)要写成 require(./data.json)
  1. module.noParse 字段告诉 Webpack 不必解析哪些文件,可以用来排除对非模块化库文件的解析.

noParse 配置项可以让 Webpack 忽略对部分没采用模块化的文件的递归解析和处理,这 样做的好处是能提高构建性能。原因是一些库如 jQuery、ChartJS 庞大又没有采用模块化标准,让 Webpack 去解析这些文件既耗时又没有意义。noParse 是可选的配置项,类型需要是 RegExp 、[RegExp]、function 中的一种。例如,若想要忽略 jQuery 、ChartJS ,则优化配置如下:

// 使用正则表达式
noParse: /jquerylchartjs/;
// 使用函数,从 Webpack3.0.0开始支持
noParse: content => {
  // 返回true或false
  return /jquery|chartjs/.test(content);
};
  1. 优化 loader 配置,通过 test、exclude、include 缩小搜索范围;
  • (1) 优化正则匹配
  • (2) 通过 cacheDirectory 选项开启缓存
  • (3) 通过 include、exclude 来减少被处理的文件
{
  // 1、如果项目源码中只有js文件,就不要写成/\.jsx?$/,以提升正则表达式的性能
  test: /\.js$/,
  // 2、babel-loader支持缓存转换出的结果,通过cacheDirectory选项开启
  loader: 'babel-loader?cacheDirectory',
  // 3、只对项目根目录下的src 目录中的文件采用 babel-loader
  include: [resolve('src')]
},


3.2、使用 DllPlugin 减少基础模块编译次数

DllPlugin 动态链接库插件,其原理是把网页依赖的基础模块抽离出来打包到dll文件中,当需要导入的模块存在于某个dll中时,这个模块不再被打包,而是去 dll 中获取。**为什么会提升构建速度呢?**原因在于 dll 中大多包含的是常用的第三方模块,如 react、react-dom,所以只要这些模块版本不升级,就只需被编译一次。我认为这样做和配置 resolve.alias 和 module.noParse 的效果有异曲同工的效果。

详情 demo 请见:lucky_vue_template

  1. 使用 DllPlugin 配置一个 webpack.dll.config.js 来构建 dll 文件:
var path = require("path");
var webpack = require("webpack");
var CleanWebpackPlugin = require("clean-webpack-plugin");

var vendor = ["vue""vuex""vue-router"];
var vendordev = ["vue/dist/vue.esm.js""vuex""vue-router"]; //集成开发版本vue
var config = require("./config/config.js");
module.exports = {
  //你想要打包的模块数组
  entry: {
    vendor: vendor,
    vendordev: vendordev
  },
  output: {
    path: path.join(__dirname, config.dllRoot),
    filename"[name].dll.js",
    library"[name]_library"
    //vendor.dll.js 中暴露出的全局变量
    //主要是给DllPlugin中的name 使用
    //故这里需要和webpack.DllPlugin 中的 'name :[name]_libray 保持一致
  },
  plugins: [
    new CleanWebpackPlugin(),
    new webpack.DllPlugin({
      path: path.join(__dirname, config.dllRoot, "[name]-manifest.json"),
      name"[name]_library",
      context: __dirname
    })
  ]
};

需要注意 DllPlugin 的参数中 name 值必须和 output.library 值保持一致,并且生成的 manifest 文件中会引用 output.library 值。

  1. 在主 config 文件里使用 DllReferencePlugin 插件引入 xxx-manifest.json 文件:
[
  new webpack.DllReferencePlugin({
    context: __dirname,
    manifestrequire(config.dllPath(truetrue))
  }),

  new HtmlWebpackTagsPlugin({ tags: ["/dll/vendor.dll.js"], appendfalse }),

  new CopyWebpackPlugin([
    {
      from: path.join(__dirname, config.dllPath(truefalse)),
      to: path.join(__dirname, config.dllVendorTarget)
    }
  ])
];

3.3、使用 externals 减少基础模块编译次数

我们在使用的 js 库如 vue 或者 react 等的时候,webpack 会将它们一起打包,react 和 react-dom 文件就好几百 k,全部打包成一个文件,可想而知,这个文件会很大,用户在首次打开时就往往会出现白屏等待时间过长的问题,这时,我们就需要将这类文件抽离出来。

externals: {
        "react""React",
        "react-dom""ReactDOM"
    },

这里我们会用到 externals,它和 plugins 是平级。左侧 key 表示依赖,右侧 value 表示文件中使用的对象。比如在 react 开发中,我们常常会这样在文件头部写 import React from 'react',这里大家可以和上面对号入座下。

这里我们就需要对这个文件进行单独的引入使用了,在 index.html 中添加如下代码

<script src="./node_modules/react/umd/react.xxx.js"></script>
<script src="./node_modules/react-dom/umd/react-dom.xxx.js"></script>

不过,我们在项目上线的时候不可能会带有 node_modules,所以我们就需要使用一个 copy 插件将 react 和 react-dom 文件复制出来

 new CopyWebpackPlugin([ // from是要copy的文件,to是生成出来的文件
            { from"node_modules/react/umd/react.xxx.js"to"js/react.min.js" },
            { from"node_modules/react-dom/umd/react-dom.xxx.js"to"js/react-dom.min.js" }
            { from"public/favicon.ico"to"favicon.ico" }
        ])

3.4、使用 HappyPack 多进程解析和处理文件

由于有大量文件需要解析和处理,所以构建是文件读写和计算密集型的操作,特别是当文件数量变多后,Webpack 构建慢的问题会显得更为严重。运行在 Node.之上的 Webpack 是单线程模型的,也就是说 Webpack 需要一个一个地处理任务,不能同时处理多个任务。Happy Pack ( https://github.com/amireh/happypack )就能让 Webpack 做到这一点,它将任务分解给多个子进程去并发执行,子进程处理完后再将结果发送给主进

配置如下

  • HappyPack 插件安装:$ npm i -D happypack

  • webpack.base.conf.js 文件对 module.rules 进行配置

    module: {
     rules: [
      {
        test/\.js$/,
        // 将对.js 文件的处理转交给 id 为 babel 的HappyPack实例
          use:['happypack/loader?id=babel'],
          include: [resolve('src'), resolve('test'),
            resolve('node_modules/webpack-dev-server/client')],
        // 排除第三方插件
          exclude:path.resolve(__dirname,'node_modules'),
        },
        {
          test/\.vue$/,
          use: ['happypack/loader?id=vue'],
        },
      ]
    },
  • (3)webpack.prod.conf.js 文件进行配置
const HappyPack = require("happypack");
// 构造出共享进程池,在进程池中包含5个子进程
const HappyPackThreadPool = HappyPack.ThreadPool({ size5 });
plugins: [
  new HappyPack({
    // 用唯一的标识符id,来代表当前的HappyPack是用来处理一类特定的文件
    id: "vue",
    loaders: [
      {
        loader"vue-loader",
        options: vueLoaderConfig
      }
    ],
    threadPool: HappyPackThreadPool
  }),

  new HappyPack({
    // 用唯一的标识符id,来代表当前的HappyPack是用来处理一类特定的文件
    id: "babel",
    // 如何处理.js文件,用法和Loader配置中一样
    loaders: ["babel-loader?cacheDirectory"],
    threadPool: HappyPackThreadPool
  })
];

3.5、 使用 ParallelUglifyPlugin 开启多进程压缩 JS 文件

由于压缩 JavaScript 代码时,需要先将代码解析成用 Object 抽象表示的 AST 语法树,再去应用各种规则分析和处理 AST ,所以导致这个过程的计算量巨大,耗时非常多。当 Webpack 有多个 JavaScript 文件需要输出和压缩时,原本会使用 UglifyJS 去一个一个压缩再输出,但是 ParallelUglifyPlugin 会开启多个子进程,将对多个文件的压缩工作分配给多个子进程去完成,每个子进程其实还是通过 UglifyJS 去压缩代码,但是变成了并行执行。所以 ParallelUglify Plugin 能更快地完成对多个文件的压缩工作。

1)ParallelUglifyPlugin插件安装:
     $ npm i -D webpack-parallel-uglify-plugin
2)webpack.prod.conf.js 文件进行配置
    const ParallelUglifyPlugin =require('webpack-parallel-uglify-plugin');
    plugins: [
    new ParallelUglifyPlugin({
      cacheDir'.cache/',
      uglifyJs:{
        compress: {
          warningsfalse
        },
        sourceMaptrue
      }
     }),
    ]


3.6、使用自动刷新

借助自动化的手段,在监听到本地源码文件发生变化时,自动重新构建出可运行的代码后再控制浏览器刷新。Webpack 将这些功能都内置了,并且提供了多种方案供我们选择。

Webpack 可以使用两种方式开启监听:

  1. 启动 webpack 时加上--watch 参数;
  2. 在配置文件中设置 watch:true

Webpack 配置官方文档点击这里

module.exports = {
  watchtrue,
  watchOptions: {
    ignored/node_modules/,
    aggregateTimeout300//文件变动后多久发起构建,越大越好
    poll: 1000 //每秒询问次数,越小越好
  }
};

相关优化措施:

  • (1)配置忽略一些不监听的一些文件,如:node_modules。
  • (2)watchOptions.aggregateTirneout 的值越大性能越好,因为这能降低重新构建的频率。
  • (3)watchOptions.poll 的值越小越好,因为这能降低检查的频率。

Vue Cli 可配置如下:

devServer: {
  watchOptions: {
    // 不监听的文件或文件夹,支持正则匹配
    ignored: /node_modules/,
    // 监听到变化后等300ms再去执行动作
    aggregateTimeout: 300,
    // 默认每秒询问1000次
    poll: 1000
  }
},


3.7、开启模块热替换

模块热替换不刷新整个网页而只重新编译发生变化的模块,并用新模块替换老模块,所以预览反应更快,等待时间更少,同时不刷新页面能保留当前网页的运行状态。原理也是向每一个 chunk 中注入代理客户端来连接 DevServer 和网页。开启方式:

  • webpack-dev-server --hot
  • 使用 HotModuleReplacementPlugin,比较麻烦

开启后如果修改子模块就可以实现局部刷新,但如果修改的是根JS文件,会整页刷新,原因在于,子模块更新时,事件一层层向上传递,直到某层的文件接收了当前变化的模块,然后执行回调函数。如果一层层向外抛直到最外层都没有文件接收,就会刷新整页。使用 NamedModulesPlugin 可以使控制台打印出被替换的模块的名称而非数字ID,另外同 webpack 监听,忽略node_modules目录的文件可以提升性能

devServer: {
  hottrue,
},
plugins: [
  new webpack.HotModuleReplacementPlugin(),
// 显示被替换模块的名称
  new webpack.NamedModulesPlugin(), // HMR shows correct file names
]



以上是关于Webpack优化构建速度的主要内容,如果未能解决你的问题,请参考以下文章

webpack构建速度优化

性能优化:Webpack 优化构建速度

优化 Webpack 的构建速度

优化 Webpack 的构建速度

记住4点优化就可提高webpack构建速度

5-webpack构建速度和体积优化策略