生产环境webpack.config.js性能优化配置

1.优化打包构建速度
1.修改loader处理

当处理一个文件的时候,会去匹配所有的loader,但是往往一个文件只需要一个loader处理,其他剩余的loader没必要花时间去匹配处理,此时就可以使用oneOf选项

使用:需要处理的loader放在oneOf数组中:

 module: {
    rules: [
      {
        oneOf: [
          {
            // css处理
            test: /\.css$/,
            use: [...commonCssPlugin]
          },
          {
            // js语法检查
            test: /\.js$/,
            exclude: /node_modules/,
            loader: 'eslint-loader',
            options: {
              fix: true
            }
          },
          {
            // js兼容性处理
            test: /\.js$/,
            exclude: /node_modules/,
            loader: 'babel-loader',
            options: {
              presets: [
                [
                  '@babel/preset-env',
                  {
                    useBuiltIns: 'usage',
                    corejs: {
                      version: 3
                    },
                    targets: {
                      chrome: '60',
                      firefox: '60',
                      ie: '9',
                      safari: '10',
                      edge: '17'
                    }
                  }
                ]
              ]
            }
          }
        ]
      }
    ]
  },

此语法的意思:当匹配到符合的文件类型,则使用该配置,后续的loader不再遍历

但是需要注意的是:不能有两个配置处理同一类型文件

以上述代码为例,有两个配置处理js文件,这是不允许的,但是不得不需要两个配置处理js文件,做法:将需要先处理的配置提取到oneOf所处对象之外,和之前写loader配置一样即可:

 module: {
    rules: [
         {
            // js语法检查
            test: /\.js$/,
            exclude: /node_modules/,
            loader: 'eslint-loader',
            options: {
              fix: true
            }
          },
      {
        oneOf: [
          {
            // js兼容性处理
            test: /\.js$/,
            exclude: /node_modules/,
            loader: 'babel-loader',
            options: {
              presets: [
                [
                  '@babel/preset-env',
                  {
                    useBuiltIns: 'usage',
                    corejs: {
                      version: 3
                    },
                    targets: {
                      chrome: '60',
                      firefox: '60',
                      ie: '9',
                      safari: '10',
                      edge: '17'
                    }
                  }
                ]
              ]
            }
          }
        ]
      }
    ]
  },
2.babel缓存

场景:babel是用来做js兼容性处理的,如果有很多js模块,只修改其中几个模块,当每次webpack打包时,都要重新编译处理,很影响打包构建速度,因此可以将babel缓存起来,也即将babel处理之后的js模块缓存起来,只对其中一个变化了的js模块重新编译,而没有变化的直接从缓存中获取。这一方式和HMR功能类似,但是为什么不使用呢?HMR基于devServer,而devServer是用于开发环境便于开发的,不用于生产环境。

作用:让第二次打包构建速度更快

使用:在babel配置中加上cacheDirectory: true

{
            // js兼容性处理
            test: /\.js$/,
            exclude: /node_modules/,
            loader: 'babel-loader',
            options: {
              presets: [
                [
                  '@babel/preset-env',
                  {
                    useBuiltIns: 'usage',
                    corejs: {
                      version: 3
                    },
                    targets: {
                      chrome: '60',
                      firefox: '60',
                      ie: '9',
                      safari: '10',
                      edge: '17'
                    }
                  }
                ]
              ],
                  ## babel缓存
              cacheDirectory: true
            }
          },
3.多进程打包

由于js引擎是单线程的,其同一时间只能干一件事,如果要完成的任务比较多的时候,其打包速度就会比较慢。此时可以采取多进程的方式进行打包

npm i -s thread-loader

使用:在需要开启多进程的处理中在最后加上thread-loader即可,一般给babel-loader使用,原因:一般Js文件比较大,其次babel相对于eslint既要检查又要编译等操作。

{
  test: /\.js$/,
  exclude: /node_modules/,
  use: [
    /*
    开启多进程打包。
    进程启动大概600ms,进程通信也有开销。
    只有工作消耗时间比较长,才需要多进程打包
     */
    {
      loader: 'thread-loader',
      options: {
        worker: 2 // 开启两个进程
      }
    },
    {
      loader: 'babel-loader',
      options: {
        presets: [
          [
            '@babel/preset-env',
            {
              useBuiltIns: 'usage',
              corejs: {
                version: 3
              },
              targets: {
                chrome: '60'
              }
            }
          ]
        ]
      }
    }
  ]
}
4.externals

当不需要将某个库文件进行打包的时候,需要拒绝其的打包。例如:通过cdn引入jquery,而不想对其打包的时候。

使用:在webpack.config.js中添加

externals: {
    // 拒绝jquery的打包
    jquery: 'jQuery'
  }

但是需要注意的是:由于没有对其打包,要使用则需要自己引入

5.dll

不同于enternals不对第三方库文件打包,dll则对第三方库文件进行单独打包操作,由于代码打包分隔配置optimization只是对第三方库文件打包成一个chunk,但是有时为了当每次打包时只要库文件不变,则不对其进行再次打包操作,则可以采取dll将目标库文件单独打包成一个chunk

用法:

新建一个文件webpack.dll.js

# webpack.dll.js

const { resolve } = require('path');
const webpack = require('webpack');

module.exports = {
  entry: {
    // 最终打包生成的[name]为jquery
    jquery: ['jQuery'] // 由于是数组,如果有相关jquery文件都可以引入
  },
  output: {
    filename: '[name].js',
    path: resolve(__dirname,'dll'),
    library: '[name]_[hash]'// 打包的库里面向外暴露出去的内容的名称
  },
  plugins: [
    // 打包生成一个manifest.json 提供和jquery映射
    new webpack.DllPlugin({
      name: '[name]_[hash]',// 映射库的暴露的内容名称
      path: resolve(__dirname,'dll/manifest.json')// 输出文件路径
    })
  ],
  mode: 'production'
}

该文件的作用:将jquery单独进行打包,然后生成的向外暴露的名称以及打包之后的文件,通过插件webpack.DllPlugin生成manifest.json文件进行映射。

因此此文件需要单独进行运行:webpack --config webpack.dll.js

运行之后生成的内容为:

dll

jquery.js // 包含向外暴露的名称以及内容

jquery.js.LICENSE.txt // 版本说明

manifest.json // 映射表

那如何将打包之后的jquery来进行使用呢?

在webpack.config.js文件中,使用两个插件

webpack.DllReferencePluginadd-asset-html-webpack-plugin

第一个插件是webpack内置的不用下载,第二个插件需要下载

npm i -s add-asset-html-webpack-plugin

使用:

// 告诉webpack哪些库不参与打包,同时使用时的名称也需要改
    new webpack.DllReferencePlugin({
      manifest: resolve(__dirname,'dll/manifset.json')
    }),
    // 将某个文件输出去,并在html中自动引入该资源
    new addAssetHtmlWebpackPlugin({
      filepath: resolve(__dirname,'dll/jquery.js')
    })

该配置的作用就是:首先告诉webpack哪些文件是不需要打包的,但是仅仅只告诉其哪些文件不打包,并没有告诉其在哪使用如何去引入使用,所以一种方式是:手动引入打包之后的jquery.js文件,其次通过第二个插件add-asset-html-webpack-plugin进行自动引入

这样,当每次webpack进行打包的时候,都不会重复去打包jquery,而是仅仅去引用即可。

总结:

  • cdn链接引用则使用externals
  • 使用自己服务器单独对第三方打包则使用dll
2.优化代码运行的性能
1.文件资源缓存

场景:一般状态下,对于js,css等文件资源,如果后台设置了一定时间的有效期,则当浏览器获取到这些资源的时候,发现当前在有效期时间内,则将文件资源存储于缓存中(Memory cache),当下次访问的时候,则直接从缓存中获取,这样用户访问速度变快。但是会涉及到一个问题:当后台源文件被修改了,但是文件资源还没有失效,则浏览器不会对文件资源再次获取,因此后台源文件的修改不会让浏览器显示部分同步变化,使得当有严重bug时不能及时修复。

解决办法:给每个文件资源后缀加上一个hash值,这样当文件名改了之后,浏览器检测到变化会再次发送请求获取新的资源

  • hash:每次webpack构建时会生成一个唯一的hash值,而采用hash方式,则会采取webpack生成的hash值来构建文件资源后缀
// 在需要输出的文件资源后缀加上hash值
output: {
    filename: 'js/index.[hash:10].js',
    path: resolve(__dirname, 'build')
  }

new miniCssExtractPlugin({
      filename: 'css/index.[hash:10].css'
    }),

问题:因为js和css使用的是同一个hash值,当只修改一个文件的时候,势必会导致所有缓存失效,这样和不使用hash的效果不大

  • chunkhash:根据chunk生成的hash值,如果打包来源于同一个chunk,那么hash值就一样。同一个chunk是:通过同一个关系结构树构造的chunk块

    // 在需要输出的文件资源后缀加上chunkhash值
    output: {
        filename: 'js/index.[chunkhash:10].js',
        path: resolve(__dirname, 'build')
      }
    
    new miniCssExtractPlugin({
          filename: 'css/index.[chunkhash:10].css'
        }),
    

    问题:由于css等资源一般都是被入口js文件引入,同属于一个chunk,当修改js文件时,其他资源缓存仍然失效

  • contenthash:根据文件内容生成hash值。不同文件hash值不一样,如果该文件没有变化则hash值也不会变,这样当修改一个文件的时候,不会影响其他文件资源缓存,让代码上线运行缓存更好使用

2.tree shaking

作用:去除无用代码,减少代码体积

前提:

  1. 当前文件必须使用了es6模块化
  2. 开启production环境

有以上的前提之后,会自动实现tree shaking功能

但是不同版本的webpack会对css处理方式不一样,可能会将css文件错误当作无效的代码去除。

比如:在package.json中配置:"sideEffects": false(表示所有代码都没有副作用,即都可以进行tree shaking),此时产生的问题是:将css/ @babel/polyfill文件都去除掉了

解决:指定不会被tree shaking的文件

"sideEffects": ["*.css"]
3.code split

将打包输出的一个bundle拆分成几个文件,比如:一个入口文件中引入了css,jquery文件,常规形成的bundle文件只有一个,此时将css,jquery等其他文件进行拆分,便于之后的按需加载。

  • 方式一:

    采取多入口文件

    // 多文件入口
    entry: {
        main: './src/index.js',
        test: './src/print.js'
      },
      output: {
        filename: 'js/[name].[contenthash:10].js',// 取entry的命名作为生成的bundle名
        path: resolve(__dirname, 'build')
      },
    

    此方式,针对于多页面项目,仅能分离出js文件,由于每次增加入口文件都需要修改配置文件,因此一般不采用此方式。

  • 方式二:

    针对于将第三方文件单独打包成一个chunk。

    • 对于单入口文件而言
    module.exports = {
      entry:'./src/index.html',
        output: {
          filename: 'main.js',
            path: resolve(__dirname,'build')
          },
        # 配置打包
        optimization: {
            splitChunks: {
                chunks: 'all'
            }
        }
    }
    

    此方式可以将第三方文件(node_module)中的代码单独打包成一个chunk输出

    • 多入口文件

      module.exports = {
          entry:{
              index: './src/index.js',
              test: './src/test.js'
          }
          output: {
              filename: '[name].js',
              path: resolve(__dirname,'build')
              },
          # 配置打包
          optimization: {
              splitChunks: {
                  chunks: 'all'
              }
          }
      }
      

      假如:index.js和test.js都引入了第三方包,此时打包之后的结果为:自动分析多文件入口chunk中,有没有公共的文件(必须有一定大小:30kb以上)。如果有会打包成一个chunk文件,也即index.js test.js jquery.js这三个chunk作为最终输出,而index.js于test.js对jquery可以进行单独引用,而不会每个入口文件打包时都分别引入第三方文件,造成代码冗余度很高代码体积很大等问题。

  • 方式二:

    针对单入口文件中需要打包除第三方文件之外的其他文件

    采用:import动态导入语法,能将某个文件单独打包,通过js代码,让某个文件被单独打包成一个chunk

    // 在入口文件中写入
    // /* webpackChunkName: 'print' */表明给这个文件固定命名为print
    import (/* webpackChunkName: 'print' */'./print')
      .then((result) => {
        // 文件加载成功
        console.log(result);// print模块导出的内容
      })
      .catch(() => {
        console.log('出错了');
      })
    
  • 总结:

    一般情况下使用方式二与方式三的结合,通过方式二将第三方文件进行打包成单独的chunk,再通过方式三将入口文件除第三方文件之外的其他文件打包成单独的chunk

4.懒加载和预加载

此处讨论的只针对于js文件

  • 懒加载:

    多个文件同时引入,但是有时只是希望在某件事情触发之后才加载该文件,因此可以通过懒加载,在真正需要使用该文件的时候才加载

    使用:采用import动态导入语法来解决懒加载

    // index.js
    console.log('index被加载了');
    
    document.querySelector('#btn').onclick = function () {
      import(/* webpackChunkName: 'test' */'./print')
        .then(({ print }) => {
          console.log(print(2,3));
        })
        .catch(() => {
          console.log('加载失败');
        })
    }
    // 只有当事件发生之后才加载print文件
    
  • 预加载

    在使用之前预先加载js文件

    正常加载:并行加载,同一个时间加载多个文件,如果文件数目增多,则会导致阻塞

    预加载:等其他资源加载完毕之后,浏览器空闲了,再加载资源,极大解决了阻塞问题

    // index.js
    console.log('index被加载了');
    // webpackPrefetch: true用于指定为预加载
    document.querySelector('#btn').onclick = function () {
      import(/* webpackChunkName: 'test' ,webpackPrefetch: true*/'./print')
        .then(({ print }) => {
          console.log(print(2,3));
        })
        .catch(() => {
          console.log('加载失败');
        })
    }
    

    注意:webpackPrefetch: true有兼容性问题,只适用于高版本的PC端,移动端的等不怎么适用,因此需要慎用

5.PWA

PWA(Progressive Web App):渐进式web应用,是一些技术的集合。作用:让网页像app一样可以离线访问。主要由service workcache完成。使用pwa使得网页在离线的情况下也能访问。其的普及使用还待发展

使用:通过workbox,在webpack中则通过workbox-webpack-plugin来完成

npm i -s workbox-webpack-plugin

使用:

  • 引入

    const workBoxWebpackPlugin = require('workbox-webpack-plugin');
    
  • webpack.config.js配置

     plugins: [
        new workBoxWebpackPlugin.GenerateSW({
          /**
           * 1. 帮助serviceWorker快速启动
           * 2. 删除旧的serviceWorker
           * 3. 通过入口文件中的配置生成一个service配置文件(通过该文件注册serviceWorker)
           */
          clientsClaim: true,
          skipWaiting: true
        })
      ],
    
  • 入口文件"index.js"配置

    /*
    1. eslint不认识window、navigator全局变量
        解决:需要修改package.json中的eslintConfig配置
        "env": {
          "browser": true
        },
     2. sw代码必须运行在服务器上
     */
    // 注册serviceWorker
    // 处理兼容性问题,可以使用则使用,否则不使用
    if ('serviceWorker' in navigator) {
      // 当页面资源加载完成之后注册servicWorker
      window.addEventListener('load', () => {
        // 该文件是由webpack.config.js中的workboxWebpackPLugin插件完成注册
        navigator.serviceWorker
          .register('/service-worker.js')
          .then(() => {
            console.log('sw注册成功');
          })
          .catch(() => {
            console.log('sw注册失败');
          });
      });
    }
    

    注意:.register('/service-worker.js')中的路径是需要写成相对于项目根目录的路径,才可以被访问成功

  • 如何知道PWA配置成功?

    开启服务运行浏览器,右键打开开发者工具,Application-> serviceWork以及Cache Storage如果有内容,则表明成功,此时把network改为offline,如果依然正常访问,则配置成功。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容