一口气(有点长)掌握Webpack性能优化

Webpack是如今主流的功能强大的模块化打包工具,在使用Webpack时,若是不注意性能优化,有很是大的可能会产生性能问题,性能问题主要分为开发时打包构建速度慢、开发调试时的重复性工做、以及输出文件质量不高等,所以性能优化也主要从这些方面来分析。本文主要是根据本身的理解对《深刻浅出 Webpack》这本书进行总结,涵盖了大部分的优化方法,能够做为Webpack性能优化时的参考和检查清单。基于Webpack3.4版本,阅读本文须要您熟悉Webpack基本使用方法。磨刀不误砍柴工,让咱们花点时间先掌握webpack性能优化吧。javascript

深刻浅出 Webpack吴浩麟css

1、优化构建速度

Webpack在启动后会根据Entry配置的入口出发,递归地解析所依赖的文件。这个过程分为搜索文件和把匹配的文件进行分析、转化的两个过程,所以能够从这两个角度来进行优化配置。html

1.1 缩小文件的搜索范围

1.1.1 优化Loader配置:

因为Loader对文件的转换操做很耗时,因此须要让尽量少的文件被Loader处理java

module.export = {
	modeule: {
		rules: [
			{
				// 若是项目源码中只有js文件,就不要写成/\.jsx?$/,以提高正则表达式的性能
				test: /\.js$/,
				// babel-loader支持缓存转换出的结果,经过cacheDirectory选项开启
				use: ['babel-loader?cacheDirectory'],
				// 只对项目根目录下的src目录中的文件采用babel-loader
				include: path.resolve(__dirname, 'src'),
			}
		]
	}
}
复制代码

1.1.2 resolve字段告诉webpack怎么去搜索文件,resolve.modules:

resolve.modules:[path.resolve(__dirname, 'node_modules')]
复制代码

避免层层查找。node

resolve.modules告诉webpack去哪些目录下寻找第三方模块,默认值为['node_modules'],会依次查找./node_modules、../node_modules、../../node_modules。react

1.1.3 resolve.mainFields:

resolve.mainFields:['main']
复制代码

设置尽可能少的值能够减小入口文件的搜索步骤 第三方模块为了适应不一样的使用环境,会定义多个入口文件,mainFields定义使用第三方模块的哪一个入口文件,因为大多数第三方模块都使用main字段描述入口文件的位置,因此能够设置单独一个main值,减小搜索jquery

1.1.4 resolve.alias:

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

resolve.alias:{
  'react':patch.resolve(__dirname,'./node_modules/react/dist/react.min.js')
}
复制代码

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

1.1.5 resolve.extensions:,减小文件查找

默认值:extensions:['.js', '.json'],当导入语句没带文件后缀时,Webpack会根据extensions定义的后缀列表进行文件查找,因此:es6

  • 列表值尽可能少
  • 频率高的文件类型的后缀写在前面
  • 源码中的导入语句尽量的写上文件后缀,如require(./data)要写成require(./data.json)

1.1.6 module.noParse字段告诉Webpack没必要解析哪些文件,能够用来排除对非模块化库文件的解析

如jQuery、ChartJS,另外若是使用resolve.alias配置了react.min.js,则也应该排除解析,由于react.min.js通过构建,已是能够直接运行在浏览器的、非模块化的文件了。noParse值能够是RegExp、[RegExp]、function

module:{ noParse:[/jquery|chartjs/, /react\.min\.js$/] }

1.1.7 配置loader时,经过test、exclude、include缩小搜索范围

1.2 使用DllPlugin减小基础模块编译次数

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

使用方法:

  1. 使用DllPlugin配置一个webpack_dll.config.js来构建dll文件:

    // webpack_dll.config.js
    const path = require('path');
    const DllPlugin = require('webpack/lib/DllPlugin');
    module.exports = {
     entry:{
         // 将react相关的模块放到一个单独的动态连接库中
         react:['react','react-dom'],
         polyfill:['core-js/fn/promise','whatwg-fetch']
     },
     output:{
         filename:'[name].dll.js',
         path:path.resolve(__dirname, 'dist'),
         library:'_dll_[name]',  //dll的全局变量名
     },
     plugins:[
         new DllPlugin({
             name:'_dll_[name]',  //dll的全局变量名
             path:path.join(__dirname,'dist','[name].manifest.json'),//描述生成的manifest文件
         })
     ]
    }
    复制代码

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

    最终构建出的文件:

    |-- polyfill.dll.js
     |-- polyfill.manifest.json
     |-- react.dll.js
     └── react.manifest.json
    复制代码

    其中xx.dll.js包含打包的n多模块,这些模块存在一个数组里,并以数组索引做为ID,经过一个变量假设为_xx_dll暴露在全局中,能够经过window._xx_dll访问这些模块。xx.manifest.json文件描述dll文件包含哪些模块、每一个模块的路径和ID。而后再在项目的主config文件里使用DllReferencePlugin插件引入xx.manifest.json文件。

  2. 在主config文件里使用DllReferencePlugin插件引入xx.manifest.json文件:

    //webpack.config.json
    const path = require('path');
    const DllReferencePlugin = require('webpack/lib/DllReferencePlugin');
    const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');
    module.exports = {
        entry:{ main:'./main.js' },
        //... 省略output、loader等的配置
        plugins:[
            new DllReferencePlugin({
                manifest:require('./dist/react.manifest.json')
            }),
            new DllReferenctPlugin({
                manifest:require('./dist/polyfill.manifest.json')
            }),
            // 须要手动引入react.dll.js
            new AddAssetHtmlWebpackPlugin(
           { filepath: path.resolve(__dirname,'dll/react.dll.js') }
            )
        ]
    }
    复制代码

    最终构建生成main.js

  3. 执行构建

    webpack --config webpack_dll.config.js
    复制代码

1.3 使用HappyPack开启多进程Loader转换

在整个构建流程中,最耗时的就是Loader对文件的转换操做了,而运行在Node.js之上的Webpack是单线程模型的,也就是只能一个一个文件进行处理,不能并行处理。HappyPack能够将任务分解给多个子进程,最后将结果发给主进程。JS是单线程模型,只能经过这种多进程的方式提升性能。

HappyPack的核心原理就是将这部分任务分解到多个进程中去并行执行,从而减小总的构建时间。

HappyPack使用以下:

npm i -D happypack
// webpack.config.json
const path = require('path');
const HappyPack = require('happypack');
// 构建出共享进程池,包含5个子进程 多个HappyPack实例都用同个进程池中的子进程处理任务,以防资源占用过多
// const happyThreadPool = HappyPack.ThreadPool({size: 5});

module.exports = {
    //...
    module:{
        rules:[{
                test:/\.js$/// 将对.js文件的处理转交给id为babel的HappyPack的实例
                use:['happypack/loader?id=babel']
                exclude:path.resolve(__dirname, 'node_modules')
            },{
                test:/\.css/,
                // 将对.css文件的处理转交给id为css的HappyPack的实例
                use:['happypack/loader?id=css']
            }],
        plugins:[
            new HappyPack({
                id:'babel',
                loaders:['babel-loader?cacheDirectory'],
                //threads: 3,// 开启几个子进程去处理这一类的文件,默认3个
                //verbose: true,// 是否容许happyPack输出日志,默认true
                //threadPool: happyThreadPool// 表明共享进程池
            }),
            new HappyPack({
                id:'css',
                loaders:['css-loader']
            })
        ]
    }
}
复制代码

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

使用UglifyJS插件压缩JS代码时,须要先将代码解析成Object表示的AST(抽象语法树),再去应用各类规则去分析和处理AST,因此这个过程计算量大耗时较多。ParallelUglifyPlugin能够开启多个子进程,每一个子进程使用UglifyJS压缩代码,能够并行执行,能显著缩短压缩时间。

使用也很简单,把原来的UglifyJS插件换成本插件便可,使用以下:

npm i -D webpack-parallel-uglify-plugin

// webpack.config.json
const ParallelUglifyPlugin = require('wbepack-parallel-uglify-plugin');
//...
plugins: [
    new ParallelUglifyPlugin({
        uglifyJS:{
            //...这里放uglifyJS的参数
        },
        //...其余ParallelUglifyPlugin的参数,设置cacheDir能够开启缓存,加快构建速度
    })
]
复制代码

经过new ParallelUglifyPlugin()实例化时,支持如下参数:

  • test
  • include
  • exclude
  • cacheDir
  • workerCount
  • sourceMap
  • uglifyES
  • uglifyJS

2、优化开发体验

开发过程当中修改源码后,须要自动构建和刷新浏览器,以查看效果。这个过程可使用Webpack实现自动化,Webpack负责监听文件的变化,DevServer负责刷新浏览器。

2.1 使用自动刷新

2.1.1 Webpack监听文件

Webpack可使用两种方式开启监听:1. 启动webpack时加上--watch参数;2. 在配置文件中设置watch:true。此外还有以下配置参数。合理设置watchOptions能够优化监听体验。

module.exports = {
    watch: true,
    watchOptions: {
        ignored: /node_modules/,
        aggregateTimeout: 300,  //文件变更后多久发起构建,越大越好
        poll: 1000,  //每秒询问次数,越小越好
    }
}
复制代码

ignored:设置不监听的目录,排除node_modules后能够显著减小Webpack消耗的内存

aggregateTimeout:文件变更后多久发起构建,避免文件更新太快而形成的频繁编译以致卡死,越大越好,下降重构频率

poll:经过向系统轮询文件是否变化来判断文件是否改变,poll为每秒询问次数,越小越好,下降检查频率

2.1.2 DevServer刷新浏览器

DevServer刷新浏览器有两种方式

  1. 向网页中注入代理客户端代码,经过客户端发起刷新
  2. 向网页装入一个iframe,经过刷新iframe实现刷新效果

默认状况下,以及 devserver: {inline:true} 都是采用第一种方式刷新页面。第一种方式DevServer由于不知道网页依赖哪些Chunk,因此会向每一个chunk中都注入客户端代码,当要输出不少chunk时,会致使构建变慢。而一个页面只须要一个客户端,因此关闭inline模式能够减小构建时间,chunk越多提高越明显。关闭方式:

  1. 启动时使用webpack-dev-server --inline false
  2. 配置 devserver:{inline:false}

关闭inline后入口网址变为http://localhost:8080/webpack-dev-server/

另外devServer.compress 参数可配置是否采用Gzip压缩,默认为false

2.2 开启模块热替换HMR

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

  1. webpack-dev-server --hot
  2. 使用HotModuleReplacementPlugin,比较麻烦

开启后若是修改子模块就能够实现局部刷新,但若是修改的是根JS文件,会整页刷新,缘由在于,子模块更新时,事件一层层向上传递,直到某层的文件接收了当前变化的模块,而后执行回调函数。若是一层层向外抛直到最外层都没有文件接收,就会刷新整页。

使用 NamedModulesPlugin 可使控制台打印出被替换的模块的名称而非数字ID,另外同webpack监听,忽略node_modules目录的文件能够提高性能。

const NamedModulesPlugin = require('webpack/lib/NamedModulesPlugin');
modules.export = {
	plugins: [
		// 显示出被替换模块的名称
		new NamedModulesPlugin()
	]
}
复制代码

3、优化输出质量-压缩文件体积

3.1 区分环境--减少生产环境代码体积

代码运行环境分为开发环境和生产环境,代码须要根据不一样环境作不一样的操做,许多第三方库中也有大量的根据开发环境判断的if else代码,构建也须要根据不一样环境输出不一样的代码,因此须要一套机制能够在源码中区分环境,区分环境以后可使输出的生产环境的代码体积减少。Webpack中使用DefinePlugin插件来定义配置文件适用的环境。

const DefinePlugin = require('webpack/lib/DefinePlugin');// 只对webpack须要处理的代码有效
//...
plugins:[
    new DefinePlugin({
        'process.env': {
            NODE_ENV: JSON.stringify('production')
        }
    })
]
复制代码

注意,JSON.stringify('production') 的缘由是,环境变量值须要一个双引号包裹的字符串,而stringify后的值是'"production"'

而后就能够在源码中使用定义的环境:

if(process.env.NODE_ENV === 'production') {
    console.log('你正在使用生产环境')
    // TODO
}else {
    console.log('你正在使用开发环境')
    // TODO
}
复制代码

当代码中使用了process时,Webpack会自动打包进process模块的代码以支持非Node.js的运行环境,这个模块的做用是模拟Node.js中的process,以支持process.env.NODE_ENV === 'production' 语句。

3.2 压缩代码-JS、ES、CSS

  1. 压缩JS:Webpack内置UglifyJS插件、ParallelUglifyPlugin

    会分析JS代码语法树,理解代码的含义,从而作到去掉无效代码、去掉日志输入代码、缩短变量名等优化。经常使用配置参数以下:

    const UglifyJSPlugin = require('webpack/lib/optimize/UglifyJsPlugin');
    //...
    plugins: [
        new UglifyJSPlugin({
            compress: {
                warnings: false,  //删除无用代码时不输出警告
                drop_console: true,  //删除全部console语句,能够兼容IE
                collapse_vars: true,  //内嵌已定义但只使用一次的变量
                reduce_vars: true,  //提取使用屡次但没定义的静态值到变量
            },
            output: {
                beautify: false, //最紧凑的输出,不保留空格和制表符
                comments: false, //删除全部注释
            }
        })
    ]
    复制代码

    使用webpack --optimize-minimize 启动webpack,能够注入默认配置的UglifyJSPlugin

  2. 压缩ES6:第三方UglifyJS插件

    随着愈来愈多的浏览器支持直接执行ES6代码,应尽量的运行原生ES6,这样比起转换后的ES5代码,代码量更少,且ES6代码性能更好。直接运行ES6代码时,也须要代码压缩,第三方的uglify-webpack-plugin提供了压缩ES6代码的功能:

    npm i -D uglify-webpack-plugin@beta //要使用最新版本的插件
    //webpack.config.json
    const UglifyESPlugin = require('uglify-webpack-plugin');
    //...
    plugins:[
        new UglifyESPlugin({
            uglifyOptions: {  //比UglifyJS多嵌套一层
                compress: {
                    warnings: false,
                    drop_console: true,
                    collapse_vars: true,
                    reduce_vars: true
                },
                output: {
                    beautify: false,
                    comments: false
                }
            }
        })
    ]
    复制代码

    另外要防止babel-loader转换ES6代码,要在.babelrc中去掉babel-preset-env,由于正是babel-preset-env负责把ES6转换为ES5。

  3. 压缩CSS:css-loader?minimize、PurifyCSSPlugin

    cssnano基于PostCSS,不只是删掉空格,还能理解代码含义,例如把color:#ff0000 转换成 color:red,css-loader内置了cssnano,只须要使用 css-loader?minimize 就能够开启cssnano压缩。

    另一种压缩CSS的方式是使用PurifyCSSPlugin,须要配合 extract-text-webpack-plugin 使用,它主要的做用是能够去除没有用到的CSS代码,相似JS的Tree Shaking。

3.3 使用Tree Shaking剔除JS死代码

Tree Shaking能够剔除用不上的死代码,它依赖ES6的import、export的模块化语法,最早在Rollup中出现,Webpack 2.0将其引入。适合用于Lodash、utils.js等工具类较分散的文件。它正常工做的前提是代码必须采用ES6的模块化语法,由于ES6模块化语法是静态的(在导入、导出语句中的路径必须是静态字符串,且不能放入其余代码块中)。若是采用了ES5中的模块化,例如module.export = {...}、require( x+y )、if (x) { require( './util' ) },则Webpack没法分析出能够剔除哪些代码。

启用Tree Shaking:

  1. 修改.babelrc以保留ES6模块化语句:

    {
        "presets": [
            [
                "env", 
                { "module": false },   //关闭Babel的模块转换功能,保留ES6模块化语法
            ]
        ]
    }
    复制代码
  2. 启动webpack时带上 --display-used-exports能够在shell打印出关于代码剔除的提示

  3. 使用UglifyJSPlugin,或者启动时使用--optimize-minimize

  4. 在使用第三方库时,须要配置 resolve.mainFields: ['jsnext:main', 'main'] 以指明解析第三方库代码时,采用ES6模块化的代码入口

4、优化输出质量--加速网络请求

4.1 使用CDN加速静态资源加载

  1. CND加速的原理

    CDN经过将资源部署到世界各地,使得用户能够就近访问资源,加快访问速度。要接入CDN,须要把网页的静态资源上传到CDN服务上,在访问这些资源时,使用CDN服务提供的URL。

    因为CDN会为资源开启长时间的缓存,例如用户从CDN上获取了index.html,即便以后替换了CDN上的index.html,用户那边仍会在使用以前的版本直到缓存时间过时。业界作法:

    • HTML文件:放在本身的服务器上且关闭缓存,不接入CDN
    • 静态的JS、CSS、图片等资源:开启CDN和缓存,同时文件名带上由内容计算出的Hash值,这样只要内容变化hash就会变化,文件名就会变化,就会被从新下载而不论缓存时间多长。

另外,HTTP1.x版本的协议下,浏览器会对于向同一域名并行发起的请求数限制在4~8个。那么把全部静态资源放在同一域名下的CDN服务上就会遇到这种限制,因此能够把他们分散放在不一样的CDN服务上,例如JS文件放在js.cdn.com下,将CSS文件放在css.cdn.com下等。这样又会带来一个新的问题:增长了域名解析时间,这个能够经过dns-prefetch来解决 <link rel='dns-prefetch' href='//js.cdn.com'> 来缩减域名解析的时间。形如**//xx.com 这样的URL省略了协议**,这样作的好处是,浏览器在访问资源时会自动根据当前URL采用的模式来决定使用HTTP仍是HTTPS协议。

  1. 总之,构建须要知足如下几点:

    • 静态资源导入的URL要变成指向CDN服务的绝对路径的URL
    • 静态资源的文件名须要带上根据内容计算出的Hash值
    • 不一样类型资源放在不一样域名的CDN上
  2. 最终配置:

    const ExtractTextPlugin = require('extract-text-webpack-plugin');
    const {WebPlugin} = require('web-webpack-plugin');
    //...
    output:{
     filename: '[name]_[chunkhash:8].js',
     path: path.resolve(__dirname, 'dist'),
     publicPatch: '//js.cdn.com/id/', //指定存放JS文件的CDN地址
    },
    module:{
     rules:[{
         test: /\.css/,
         use: ExtractTextPlugin.extract({
             use: ['css-loader?minimize'],
             publicPatch: '//img.cdn.com/id/', //指定css文件中导入的图片等资源存放的cdn地址
         }),
     },{
        test: /\.png/,
        use: ['file-loader?name=[name]_[hash:8].[ext]'], //为输出的PNG文件名加上Hash值 
     }]
    },
    plugins:[
      new WebPlugin({
         template: './template.html',
         filename: 'index.html',
         stylePublicPath: '//css.cdn.com/id/', //指定存放CSS文件的CDN地址
      }),
     new ExtractTextPlugin({
         filename:`[name]_[contenthash:8].css`, //为输出的CSS文件加上Hash
     })
    ]
    复制代码

咱们但愿经过cdn的方式引入资源

const AddAssetHtmlCdnPlugin = require('add-asset-html-cdn-webpack-plugin')
new AddAssetHtmlCdnPlugin(true,{
    'jquery':'https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js'
})
复制代码

可是在代码中还但愿引入jquery来得到提示

import $ from 'jquery'
console.log('$',$)
复制代码

可是打包时依然会将jquery进行打包

externals:{
  'jquery':'$'
}
复制代码

在配置文件中标注jquery是外部的,这样打包时就不会将jquery进行打包了

4.2 多页面应用提取页面间公共代码,以利用缓存

  1. 原理

    大型网站一般由多个页面组成,每一个页面都是一个独立的单页应用,多个页面间确定会依赖一样的样式文件、技术栈等。若是不把这些公共文件提取出来,那么每一个单页打包出来的chunk中都会包含公共代码,至关于要传输n份重复代码。若是把公共文件提取出一个文件,那么当用户访问了一个网页,加载了这个公共文件,再访问其余依赖公共文件的网页时,就直接使用文件在浏览器的缓存,这样公共文件就只用被传输一次。

  2. 应用方法

    1. 把多个页面依赖的公共代码提取到common.js中,此时common.js包含基础库的代码

      const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
      //...
      plugins:[
          new CommonsChunkPlugin({
              chunks:['a','b'], //从哪些chunk中提取
              name:'common',  // 提取出的公共部分造成一个新的chunk
          })
      ]
      复制代码
    2. 找出依赖的基础库,写一个base.js文件,再与common.js提取公共代码到base中,common.js就剔除了基础库代码,而base.js保持不变

      //base.js
      import 'react';
      import 'react-dom';
      import './base.css';
      //webpack.config.json
      entry:{
          base: './base.js'
      },
      plugins:[
          new CommonsChunkPlugin({
              chunks:['base','common'],
              name:'base',
              //minChunks:2, 表示文件要被提取出来须要在指定的chunks中出现的最小次数,防止common.js中没有代码的状况
          })        
      ]
      复制代码
    3. 获得基础库代码base.js,不含基础库的公共代码common.js,和页面各自的代码文件xx.js。

      页面引用顺序以下:base.js--> common.js--> xx.js

      base.js是为了长期缓存

4.3 分割代码以按需加载

  1. 原理

    单页应用的一个问题在于使用一个页面承载复杂的功能,要加载的文件体积很大,不进行优化的话会致使首屏加载时间过长,影响用户体验。作按需加载能够解决这个问题。具体方法以下:

    1. 将网站功能按照相关程度划分红几类
    2. 每一类合并成一个Chunk,按需加载对应的Chunk
    3. 例如,只把首屏相关的功能放入执行入口所在的Chunk,这样首次加载少许的代码,其余代码要用到的时候再去加载。最好提早预估用户接下来的操做,提早加载对应代码,让用户感知不到网络加载
  2. 作法

    一个最简单的例子:网页首次只加载main.js,网页展现一个按钮,点击按钮时加载分割出去的show.js,加载成功后执行show.js里的函数

    //main.js
    document.getElementById('btn').addEventListener('click',function(){
        import(/* webpackChunkName:"show" */ './show').then((show)=>{
            show('Webpack');
        })
    })
    //show.js
    module.exports = function (content) {
        window.alert('Hello ' + content);
    }
    复制代码

    import(/* webpackChunkName:show */ './show').then() 是实现按需加载的关键,Webpack内置对import( *)语句的支持,Webpack会以./show.js为入口从新生成一个Chunk。代码在浏览器上运行时只有点击了按钮才会开始加载show.js,且import语句会返回一个Promise,加载成功后能够在then方法中获取加载的内容。这要求浏览器支持Promise API,对于不支持的浏览器,须要注入Promise polyfill。/* webpackChunkName:show */ 是定义动态生成的Chunk的名称,默认名称是[id].js,定义名称方便调试代码。为了正确输出这个配置的ChunkName,还须要配置Webpack:

    //...
    output:{
        filename:'[name].js',
        chunkFilename:'[name].js', //指定动态生成的Chunk在输出时的文件名称
    }
    复制代码

    书中另外提供了更复杂的ReactRouter中异步加载组件的实战场景。P212

5、优化输出质量--提高代码运行时的效率

5.1 使用Prepack提早求值

  1. 原理:

    Prepack是一个部分求值器,编译代码时提早将计算结果放到编译后的代码中,而不是在代码运行时才去求值。经过在编译阶段预先执行源码来获得执行结果,再直接将运行结果输出以提高性能。可是如今Prepack还不够成熟,用于线上环境还为时过早。

  2. 使用方法

    const PrepackWebpackPlugin = require('prepack-webpack-plugin').default;
    module.exports = {
        plugins:[
            new PrepackWebpackPlugin()
        ]
    }
    复制代码

5.2 使用Scope Hoisting

  1. 原理

    译做“做用域提高”,是在Webpack3中推出的功能,它分析模块间的依赖关系,尽量将被打散的模块合并到一个函数中,但不能形成代码冗余,因此只有被引用一次的模块才能被合并。因为须要分析模块间的依赖关系,因此源码必须是采用了ES6模块化的,不然Webpack会降级处理不采用Scope Hoisting。

  2. 使用方法

    const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin');
    //...
    plugins:[
        // 开启Scope Hoisting
        new ModuleConcatenationPlugin();
    ],
    resolve:{
        // 针对npm中的第三方模块优先采用jsnext:main中指向的es6模块化语法的文件
        mainFields:['jsnext:main','browser','main']
    复制代码

}

`webpack --display-optimization-bailout` 输出日志中会提示哪一个文件致使了降级处理

3. **例子**

```javascript
let a = 1;
let b = 2;
let c = 3;
let d = a+b+c
export default d;
// 引入d
import d from './d';
console.log(d)
复制代码

最终打包后的结果会变成 console.log(6)

  • 代码量明显减小
  • 减小多个函数后内存占用也将减小

6、使用输出分析工具

启动Webpack时带上这两个参数能够生成一个json文件,输出分析工具大多依赖该文件进行分析:

webpack --profile --json > stats.json 其中 --profile 记录构建过程当中的耗时信息,--json 以JSON的格式输出构建结果,>stats.json 是UNIX / Linux系统中的管道命令,含义是将内容经过管道输出到stats.json文件中。

  1. 官方工具Webpack Analyse

    打开该工具的官网webpack.github.io/anal...,就能够获得分析结果

  2. webpack-bundle-analyzer

    可视化分析工具,比Webapck Analyse更直观。使用也很简单:

    1. npm i -g webpack-bundle-analyzer安装到全局
    2. 按照上面方法生成stats.json文件
    3. 在项目根目录执行webpack-bundle-analyzer ,浏览器会自动打开结果分析页面。
npm install --save-dev webpack-bundle-analyzer
复制代码

使用插件

const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer');
mode !== "development" && new BundleAnalyzerPlugin()
复制代码

默认就会展示当前应用的分析图表

7、侧重优化开发体验的配置文件 webpack.config.js

const path = require('path');
const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
const {AutoWebPlugin} = require('web-webpack-plugin');
const HappyPack = require('happypack');

// 自动寻找 pages 目录下的全部目录,把每个目录当作一个单页应用
const autoWebPlugin = new AutoWebPlugin('./src/pages', {
  // HTML 模版文件所在的文件路径
  template: './template.html',
  // 提取出全部页面公共的代码
  commonsChunk: {
    // 提取出公共代码 Chunk 的名称
    name: 'common',
  },
});

module.exports = {
  // AutoWebPlugin 会找为寻找到的全部单页应用,生成对应的入口配置,
  // autoWebPlugin.entry 方法能够获取到生成入口配置
  entry: autoWebPlugin.entry({
    // 这里能够加入你额外须要的 Chunk 入口
    base: './src/base.js',
  }),
  output: {
    filename: '[name].js',
  },
  resolve: {
    // 使用绝对路径指明第三方模块存放的位置,以减小搜索步骤
    // 其中 __dirname 表示当前工做目录,也就是项目根目录
    modules: [path.resolve(__dirname, 'node_modules')],
    // 针对 Npm 中的第三方模块优先采用 jsnext:main 中指向的 ES6 模块化语法的文件,使用 Tree Shaking 优化
    // 只采用 main 字段做为入口文件描述字段,以减小搜索步骤
    mainFields: ['jsnext:main', 'main'],
  },
  module: {
    rules: [
      {
        // 若是项目源码中只有 js 文件就不要写成 /\.jsx?$/,提高正则表达式性能
        test: /\.js$/,
        // 使用 HappyPack 加速构建
        use: ['happypack/loader?id=babel'],
        // 只对项目根目录下的 src 目录中的文件采用 babel-loader
        include: path.resolve(__dirname, 'src'),
      },
      {
        test: /\.js$/,
        use: ['happypack/loader?id=ui-component'],
        include: path.resolve(__dirname, 'src'),
      },
      {
        // 增长对 CSS 文件的支持
        test: /\.css$/,
        use: ['happypack/loader?id=css'],
      },
    ]
  },
  plugins: [
    autoWebPlugin,
    // 使用 HappyPack 加速构建
    new HappyPack({
      id: 'babel',
      // babel-loader 支持缓存转换出的结果,经过 cacheDirectory 选项开启
      loaders: ['babel-loader?cacheDirectory'],
    }),
    new HappyPack({
      // UI 组件加载拆分
      id: 'ui-component',
      loaders: [{
        loader: 'ui-component-loader',
        options: {
          lib: 'antd',
          style: 'style/index.css',
          camel2: '-'
        }
      }],
    }),
    new HappyPack({
      id: 'css',
      // 如何处理 .css 文件,用法和 Loader 配置中同样
      loaders: ['style-loader', 'css-loader'],
    }),
    // 提取公共代码
    new CommonsChunkPlugin({
      // 从 common 和 base 两个现成的 Chunk 中提取公共的部分
      chunks: ['common', 'base'],
      // 把公共的部分放到 base 中
      name: 'base'
    }),
  ],
  watchOptions: {
    // 使用自动刷新:不监听的 node_modules 目录下的文件
    ignored: /node_modules/,
  }
};

复制代码

8、侧重优化输出质量的配置文件 webpack-dist.config.js:

const path = require('path');
const DefinePlugin = require('webpack/lib/DefinePlugin');
const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin');
const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const {AutoWebPlugin} = require('web-webpack-plugin');
const HappyPack = require('happypack');
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');

// 自动寻找 pages 目录下的全部目录,把每个目录当作一个单页应用
const autoWebPlugin = new AutoWebPlugin('./src/pages', {
  // HTML 模版文件所在的文件路径
  template: './template.html',
  // 提取出全部页面公共的代码
  commonsChunk: {
    // 提取出公共代码 Chunk 的名称
    name: 'common',
  },
  // 指定存放 CSS 文件的 CDN 目录 URL
  stylePublicPath: '//css.cdn.com/id/',
});

module.exports = {
  // AutoWebPlugin 会找为寻找到的全部单页应用,生成对应的入口配置,
  // autoWebPlugin.entry 方法能够获取到生成入口配置
  entry: autoWebPlugin.entry({
    // 这里能够加入你额外须要的 Chunk 入口
    base: './src/base.js',
  }),
  output: {
    // 给输出的文件名称加上 Hash 值
    filename: '[name]_[chunkhash:8].js',
    path: path.resolve(__dirname, './dist'),
    // 指定存放 JavaScript 文件的 CDN 目录 URL
    publicPath: '//js.cdn.com/id/',
  },
  resolve: {
    // 使用绝对路径指明第三方模块存放的位置,以减小搜索步骤
    // 其中 __dirname 表示当前工做目录,也就是项目根目录
    modules: [path.resolve(__dirname, 'node_modules')],
    // 只采用 main 字段做为入口文件描述字段,以减小搜索步骤
    mainFields: ['jsnext:main', 'main'],
  },
  module: {
    rules: [
      {
        // 若是项目源码中只有 js 文件就不要写成 /\.jsx?$/,提高正则表达式性能
        test: /\.js$/,
        // 使用 HappyPack 加速构建
        use: ['happypack/loader?id=babel'],
        // 只对项目根目录下的 src 目录中的文件采用 babel-loader
        include: path.resolve(__dirname, 'src'),
      },
      {
        test: /\.js$/,
        use: ['happypack/loader?id=ui-component'],
        include: path.resolve(__dirname, 'src'),
      },
      {
        // 增长对 CSS 文件的支持
        test: /\.css$/,
        // 提取出 Chunk 中的 CSS 代码到单独的文件中
        use: ExtractTextPlugin.extract({
          use: ['happypack/loader?id=css'],
          // 指定存放 CSS 中导入的资源(例如图片)的 CDN 目录 URL
          publicPath: '//img.cdn.com/id/'
        }),
      },
    ]
  },
  plugins: [
    autoWebPlugin,
    // 开启ScopeHoisting
    new ModuleConcatenationPlugin(),
    // 使用HappyPack
    new HappyPack({
      // 用惟一的标识符 id 来表明当前的 HappyPack 是用来处理一类特定的文件
      id: 'babel',
      // babel-loader 支持缓存转换出的结果,经过 cacheDirectory 选项开启
      loaders: ['babel-loader?cacheDirectory'],
    }),
    new HappyPack({
      // UI 组件加载拆分
      id: 'ui-component',
      loaders: [{
        loader: 'ui-component-loader',
        options: {
          lib: 'antd',
          style: 'style/index.css',
          camel2: '-'
        }
      }],
    }),
    new HappyPack({
      id: 'css',
      // 如何处理 .css 文件,用法和 Loader 配置中同样
      // 经过 minimize 选项压缩 CSS 代码
      loaders: ['css-loader?minimize'],
    }),
    new ExtractTextPlugin({
      // 给输出的 CSS 文件名称加上 Hash 值
      filename: `[name]_[contenthash:8].css`,
    }),
    // 提取公共代码
    new CommonsChunkPlugin({
      // 从 common 和 base 两个现成的 Chunk 中提取公共的部分
      chunks: ['common', 'base'],
      // 把公共的部分放到 base 中
      name: 'base'
    }),
    new DefinePlugin({
      // 定义 NODE_ENV 环境变量为 production 去除 react 代码中的开发时才须要的部分
      'process.env': {
        NODE_ENV: JSON.stringify('production')
      }
    }),
    // 使用 ParallelUglifyPlugin 并行压缩输出的 JS 代码
    new ParallelUglifyPlugin({
      // 传递给 UglifyJS 的参数
      uglifyJS: {
        output: {
          // 最紧凑的输出
          beautify: false,
          // 删除全部的注释
          comments: false,
        },
        compress: {
          // 在UglifyJs删除没有用到的代码时不输出警告
          warnings: false,
          // 删除全部的 `console` 语句,能够兼容ie浏览器
          drop_console: true,
          // 内嵌定义了可是只用到一次的变量
          collapse_vars: true,
          // 提取出出现屡次可是没有定义成变量去引用的静态值
          reduce_vars: true,
        }
      },
    }),
  ]
};

复制代码

9、后记

因为字数的限制,有些内容没法写了,掘金的一大坑啊||-_-

相关文章
相关标签/搜索