工作中的项目实践系列---前端性能优化(webpack篇)

前言

开发CDE项目的时候,每次打包构建项目的时间都比较久,如下图所示,开发环境下需要一分钟的时间才能构建完成,
在这里插入图片描述
同时开发环境下实时打包编译的时间也比较久,如下图所示,接近10s。阅读过该项目webpack.config.js的配置后,感觉有优化的空间,为了提升开发效率,遂对webpack的相关配置进行重构。
在这里插入图片描述

优化一:合并提取webpack公共配置

webpack配置分为开发环境配置和生产环境配置,在两种环境的配置文件中,存在大量的重复配置,也有部分不同的配置,如在开发阶段,我们为了提升运行效率以及调试效率, 一般会通过dev-server来实时打包,这样就无需每次在终端输入脚本命令打包,而在上线阶段我们需要拿到真实的打包文件, 所以不会通过dev-server来打包。为了提升打包效率,开发阶段不会对打包的内容进行压缩;而在上线阶段,为了提升访问的效率,在打包时需要对打包的内容进行压缩。

旧配置文件的问题及相应的改进

旧配置文件将开发环境和线上环境的配置都写到了一个文件中, 这样非常不利于我们去维护配置文件,所以我们需要针对不同的环境将不同的配置写到不同的文件中, 我们可以在根路径下创建如下目录结构,将两种环境下共有的配置抽取到webpack.common.js文件中,webpack.dev.js文件和webpack.prod.js文件分别存放开发环境和生产环境下特有的配置
在这里插入图片描述
开发环境与生产环境各自的特有配置与共有配置的合并,具体需要用到webpack-merge模块来处理。

关键代码示例

webpack.dev.js

const {
    
     merge } = require("webpack-merge");
const CommonConfig = require("./webpack.common.js");
module.exports = merge(CommonConfig, DevConfig);

webpack.prod.js

const {
    
     merge } = require("webpack-merge");
const CommonConfig = require("./webpack.common.js");
module.exports = merge(CommonConfig, ProdConfig);

package.json

"scripts": {
    
    
    "dev": "webpack-dev-server --config webpack-config/webpack.dev.js",
    "build": "webpack --config webpack-config/webpack.prod.js"
}

优化二:HappyPack实现多进程打包(开发环境和生产环境均适用)

运行在Node.js之上的Webpack是单线程模型的,也就是说Webpack需要一个一个地处理任务,不能同时处理多个任务。而HappyPack可以让Webpack 在同一时刻处理多个任务,发挥多核CPU电脑的功能,提升构建速度。它将任务分解给多个子进程去并发执行,子进程处理完后再将结果发送给主进程。由于 JavaScript 是单线程模型,所以要想发挥多核 CPU 的功能,就只能通过多进程实现,而无法通过多线程实现。
在实际使用时,要用HappyPack提供的loader来替换原有loader,并将原有的那个通过HappyPack插件传进去。
下面我们使用HappyPack对旧配置文件进行改造:
初始Webpack配置(使用HappyPack前)

module.exports = {
    
      
  //...  
  module: {
    
        
    rules: [
      {
    
    
        test: /\.ts(x)?$/,
        include: [path.resolve(__dirname, 'src')],
        use: ['babel-loader', 'ts-loader'],
      },   
    ],  
  },
}

webpack.common.js (使用HappyPack的配置)

const HappyPack = require('happypack');
module.exports = {
    
      
  //...  
  module: {
    
        
    rules: [      
       {
    
    
         test: /\.ts(x)?$/,
         include: [path.resolve(__dirname, '../src')], 
         // 把对 .ts(x) 文件的处理转交给 id 为 ts 的 HappyPack 实例
         use: ['happypack/loader?id=ts']
       }, 
    ], 
  },  
  plugins: [    
    new HappyPack({
    
    
      // 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
        id: 'ts',
      // loaders属性表示如何处理 .ts(x) 文件,用法和 Loader 配置中一样
        loaders: ['babel-loader','ts-loader']              
  ],
};

在loader的配置中,将对文件的处理交给happypack/loader,并且跟着的queryString ( id=ts )告诉happypack-loader去选择哪个happypack实例处理文件;

在plugin的配置中,新增happypack实例告诉happypack/loader如何去处理ts(x)文件,选项中的id属性值和上面的queryString(id=babel)是对应的,选项中loaders的属性和没有使用happypack前的loader的配置保持一致;

改进效果:

经过happyPack的处理后,开发环境下的构建时间由60多秒降至20多秒,打包构建所花的时间是之前的一半不到,提升打包性能的效果显著。

关于开启多进程的注意点:

开启多进程不是一定能够提高构建速度的,开启的子进程也不是越多越好,因为进程要启动,要销毁,进程之间要通讯,这个进程的开销也是比较大的。对于较大的项目,打包较慢,开启多进程能提高速度;对于较小的项目,打包很快,开启多进程会降低速度。
HappyPack的threads参数用于配置开启子进程的个数,默认是3个,笔者配置开启4个子进程的时候,构建速度和3个子进程的时候几乎没有差异,开启5个子进程的时候,构建速度比3个和4个的时候更慢。所以是否开启多进程,开启多少个进程,我们应当按需使用。

优化三:样式文件抽离和压缩(生产环境适用)

对于样式文件的处理,loader链中如果最后使用style-loader来处理,我们的css是直接打包进js里面的。生产环境下,我们希望能单独生成css文件。因为单独生成css,css可以和js并行下载,提高页面加载效率。同时,生产环境下也需要对css文件进行压缩处理,以减小文件体积,提高页面加载效率。而开发环境下,因为对文件的抽离和压缩比较耗时,为了提高打包构建的速度,以尽快开发,开发环境下,我们对样式文件不抽离不压缩。

旧配置文件的问题及相应的改进

原配置文件中,不论是开发环境还是生产环境scss文件最后一步都是通过style-loader进行处理,这是不合理的,那么下面我们对生产环境下的scss文件的处理进行改进,我们使用mini-css-extract-plugin抽离样式文件,使用
css-minimizer-webpack-plugin压缩样式文件,由于css-minimizer-webpack-plugin需要配置webpack的optimization.minimizer,这样会覆盖默认的JS压缩选项, 导致生产环境下的JS代码不被压缩了,所以JS代码也需要通过插件terser-webpack-plugin自己压缩;

代码示例如下:
webpack.prod.js

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const TerserJSPlugin = require('terser-webpack-plugin')
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin")

module.exports = {
    
      
  //...
   module: {
    
        
    rules: [
          {
    
    
              test: /\.(sass|scss)$/,
              use:[
                // 使用MiniCssExtractPlugin.loader代替style-loader,抽离css文件
                  MiniCssExtractPlugin.loader,
                  'css-loader',
                  'sass-loader', {
    
    
                    loader: 'sass-resources-loader',
                    options: {
    
    
                      resources: path.join(
                        srcPath,
                        'styles/_variables.scss',
                      ),
                    },
               }]
          }   
        ],  
   },
   plugins: [
        // 抽离 css 文件
        new MiniCssExtractPlugin({
    
     
            filename:'css/main.[contenthash:8].css'
        }),
        
    ],
   optimization: {
    
    
      // 压缩 css
      //因为覆盖了原本的配置,所以只压缩css的话,js就不被压缩了,所以需要让js也压缩;
         minimizer: [new TerserJSPlugin(), new CssMinimizerPlugin()],
   }
  
} 

优化四:Dll动态链接库(开发环境适用)

在开发环境下,每次打包构建项目的时候,react,react-dom,antd等这些不会发生变化的第三方库都会被重新打包一次,而Dll动态链接库通过提前把这些不会发生变化的第三方模块打包到dll文件中,等到在开发环境下打包构建的时候,直接去dll文件中获取,不再打包这些模块,这样第三方模块只打包了一次,避免了反复打包不会发生变化的第三方模块,提升了webpack的打包效率。
注意,dllPlugin只适用于开发环境,因为生产环境下打包一次就完了,没有必要用于生产环境。

dll动态链接库使用步骤:

1.单独配置一个webpack.dll.js文件, 专门用于打包不会变化的第三方库
在这里插入图片描述
webpack.dll.js

module.exports = {
    
    
  mode: 'production',
  // JS 执行入口文件
  entry: {
    
    
    // 把 React, antd相关模块的放到一个单独的动态链接库
    vendor: ['react', 'react-dom','antd','react-router-dom'],
  },
  output: {
    
    
    // 输出的动态链接库的文件名称,[name] 代表当前动态链接库的名称,
    // 也就是 entry 中配置的 vendor
    filename: '[name].dll.js',
    // 输出的文件都放到 dll 目录下
    path: path.join(__dirname, '..', 'dll'),
    // library表示打包的是一个库,存放动态链接库的全局变量名称,
    // 例如对应 vendor 来说就是 _dll_vendor
    // 之所以在前面加上 _dll_ 是为了防止全局变量冲突  
    library: '_dll_[name]',
  }
}

2.在打包项目的配置文件中, 通过add-asset-html-webpack-plugin将提前打包好的库插入到html中
webpack.dev.js

const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin')
module.exports = {
    
    
//..
plugins: [
   new AddAssetHtmlWebpackPlugin({
    
    
     filepath:path.join(__dirname, '..', 'dll/vendor.dll.js'),
   })
}

3.在专门打包第三方的配置文件中添加生成清单文件manifest.json的配置(manifest.json文件清楚地描述了与其对应的vendor.dll.js文件中包含哪些模块,以及每个模块的路径和ID,是一个索引文件)
webpack.dll.js

const DllPlugin = require('webpack/lib/DllPlugin')
module.exports = {
    
    
//..
plugins: [
    // 接入 DllPlugin
    new DllPlugin({
    
    
      // 动态链接库的全局变量名称,需要和 output.library 中保持一致
      // 该字段的值也就是输出的 manifest.json 文件 中 name 字段的值
      // 例如 vendor.manifest.json 中就有 "name": "_dll_vendor"
      name: '_dll_[name]',
      // 描述动态链接库的 manifest.json 文件输出时的文件名称
      path: path.join(__dirname, '..', 'dll/[name].manifest.json'),
    }),
  ]
}

4.在打包项目的配置文件中, 告诉webpack打包第三方库的时候先从哪个清单文件中查询,如果清单包含当前用到的第三方库就不打包了,因为已经在html中手动引入了
webpack.dev.js

const DllReferencePlugin = require('webpack/lib/DllReferencePlugin');
module.exports = {
    
    
//..
 new DllReferencePlugin({
    
               
     manifest: require(path.join(__dirname, '..', 'dll/vendor.manifest.json'))
   }),
}

5.vendor打包
在package.json中配置一条npm script

{
    
      
  ... 
 "scripts": {
    
       
     "dll": "webpack --config webpack-config/webpack.dll.js",
  }
}

运行yarn dll后会生成一个dll目录,里面有两个文件vendor.dll.js和vendor.manifest.json,前者包含了库的代码,后者则是资源清单。
6.开发环境下执行构建项目
运行yarn dev

改进效果

经过动态链接库dll的配置后,笔者发现项目构建的时间又减少了约2秒,一定程度上提高了构建效率。

优化五:babel-loader开启缓存提高二次构建速度(开发环境适用)

babel-loader 可以利用指定文件夹缓存经过 babel 处理好的模块,这样第二次编译的时候,对没有改动的部分直接用缓存,不会再次编译。
webpack.dev.js

module.exports = {
    
      
  //...  
  module: {
    
        
    rules: [
      {
    
    
        test: /\.ts(x)?$/,
        include: [path.resolve(__dirname, 'src')],
        // 当有设置cacheDirectory时,指定的目录将用来缓存 loader 的执行结果。
        // 如下配置,loader 将使用默认的缓存目录 node_modules/.cache/babel-loader
        use: ['babel-loader?cacheDirectory', 'ts-loader'],
      },   
    ],  
  },
}

优化六:选择合适的Source Map

当 webpack 打包源代码时,可能会很难追踪到错误和警告在源代码中的原始位置, Source Map可以将编译后的代码映射回原始源代码。Source Map 本质上是一个信息文件,里面储存着代码转换前后的对应位置信息。它记录了转换压缩后的代码所对应的转换前的源代码位置,是源代码和生产代码的映射。 Source Map 解决了在打包过程中,代码经过压缩,去空格以及 babel 编译转化后,由于代码之间差异性过大,造成无法debug的问题。
Source Map的配置:
JavaScript的Source Map的配置很简单,只要在webpack配置中添加devtool即可。
对于css、scss、less来说,则需要添加额外的Source Map配置项。如下面例子所示:

module.exports = {
    
        
  // ...    
  devtool: 'source-map',    
  module: {
    
            
    rules: [            
      {
    
                    
        test: /\.scss$/,                
        use: [                    
          'style-loader',
          {
    
    
            loader: 'css-loader',
            options: {
    
    
               sourceMap: true,                        
            },                    
          }, 
          {
    
                            
            loader: 'sass-loader',                        
            options: {
    
                                
              sourceMap: true,                        
            },                    
          }                
        ] ,            
      }        
    ],    
  },
}

JavaScript的Source Map配置的选择:
Webpack支持多种Source Map的形式。除了配置为devtool:'source-map’以外,还可以根据不同的需求选择cheap-source-map、eval-source-map等。通常它们都是source map的一些简略版本,因为生成完整的source map会延长整体构建时间,如果对打包速度需求比较高的话,建议选择一个简化版的source map。比如,在开发环境中,eval-cheap-module-source-map通常是一个不错的选择,属于打包速度和源码信息还原程度的一个良好折中。
下面介绍下Source Map配置的常见的各个组成部分的含义:
(1) eval:
不会单独生成Source Map文件, 会将映射关系存储到打包的文件中, 并且通过eval存储
(2) source-map:
会单独生成Source Map文件, 通过单独文件来存储映射关系
(3) inline:
不会单独生成Source Map文件, 会将映射关系存储到打包的文件中, 并且通过base64字符串形式存储
(4) cheap:
生成的映射信息只能定位到错误行不能定位到错误列
(5) module:
不仅希望存储我们代码的映射关系, 还希望存储第三方模块映射关系, 以便于第三方模块出错时也能更好的排错

eval-cheap-module-source-map只需要行错误信息, 并且包含第三方模块错误信息, 并且不会生成单独Source Map文件。在开发环境下不会做代码压缩,所以在Source Map中即使没有列信息,也不会影响断点调试。因为生成这种 Source Map 的速度也较快。所以在开发环境下我们将devtool设置成cheap-module-eval-source-map

改进效果:

项目原配置文件开发环境下的Source Map选择的是’inline-source-map’, 经笔者实践,开发环境下,使用’inline-source-map’,修改某一处代码,实时编译的速度在3秒以上,改为使用’eval-cheap-module-source-map’,对代码做同样的修改,实时编译的速度在1-2秒,速度有明显的提升。

猜你喜欢

转载自blog.csdn.net/m0_57307213/article/details/126982614