经常使用的webpack优化方法

1. 前言

关于webpack,相信如今的前端开发人员必定不会陌生,由于它已经成为前端开发人员必不可少的一项技能,它的官方介绍以下:javascript

webpack 是一个模块打包器。webpack的主要目标是将 JavaScript 文件打包在一块儿,打包后的文件用于在浏览器中使用,但它也可以胜任转换(transform)、打包(bundle)或包裹(package)任何资源(resource or asset)。css

在平常开发工做中,咱们除了会使用webpack以及会编写它的配置文件以外,咱们还须要了解一些关于webpack性能优化的方法,这样在实际工做就可以如虎添翼,加强自身的竞争力。html

关于webpack优化的方法我将其分为两大类,以下:前端

  • 能够提升webpack打包速度,减小打包时间的优化方法
  • 可让 Webpack 打出来的包体积更小的优化方法

OK,废话很少说,接下来咱们就来分别了解一下优化方法。vue

2. 提升 Webpack 打包速度

2.1 优化Loader搜索范围

对于 Loader 来讲,影响打包效率首当其冲必属 Babel 了。由于 Babel 会将代码转为字符串生成 AST,而后对 AST 继续进行转变最后再生成新的代码,项目越大,转换代码越多,效率就越低。固然了,咱们是有办法优化的。java

首先咱们能够优化 Loader 的文件搜索范围,在使用loader时,咱们能够指定哪些文件不经过loader处理,或者指定哪些文件经过loader处理。node

module.exports = {
  module: {
    rules: [
      {
        // js 文件才使用 babel
        test: /\.js$/,
        use: ['babel-loader'],
        // 只处理src文件夹下面的文件
        include: path.resolve('src'),
        // 不处理node_modules下面的文件
        exclude: /node_modules/
      }
    ]
  }
}

对于 Babel 来讲,咱们确定是但愿只做用在 JS 代码上的,而后 node_modules 中使用的代码都是编译过的,因此咱们也彻底没有必要再去处理一遍。react

另外,对于babel-loader,咱们还能够将 Babel 编译过的文件缓存起来,下次只须要编译更改过的代码文件便可,这样能够大幅度加快打包时间。jquery

loader: 'babel-loader?cacheDirectory=true'

2.2 cache-loader缓存loader处理结果

在一些性能开销较大的 loader 以前添加 cache-loader,以将处理结果缓存到磁盘里,这样下次打包能够直接使用缓存结果而不须要从新打包。webpack

module.exports = {
  module: {
    rules: [
      {
        // js 文件才使用 babel
        test: /\.js$/,
        use: [
          'cache-loader',
          ...loaders
        ],
      }
    ]
  }
}

那这么说的话,我给每一个loder前面都加上cache-loader,然而凡事物极必反,保存和读取这些缓存文件会有一些时间开销,因此请只对性能开销较大的 loader 使用 cache-loader。关于这个cache-loader更详细的使用方法请参照这里cache-loader

2.3 使用多线程处理打包

受限于Node是单线程运行的,因此 Webpack 在打包的过程当中也是单线程的,特别是在执行 Loader 的时候,长时间编译的任务不少,这样就会致使等待的状况。那么咱们可使用一些方法将 Loader 的同步执行转换为并行,这样就能充分利用系统资源来提升打包速度了。

2.3.1 HappyPack

happypack ,快乐的打包。人如其名,就是可以让Webpack把打包任务分解给多个子线程去并发的执行,子线程处理完后再把结果发送给主线程。

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: 'js', //ID是标识符的意思,ID用来代理当前的happypack是用来处理一类特定的文件的
        threads: 4, //你要开启多少个子进程去处理这一类型的文件
        loaders: [ 'babel-loader' ]
    }),
    new HappyPack({
        id: 'css',
        threads: 2,
        loaders: [ 'style-loader', 'css-loader' ]
    })
]

2.3.2 thread-loader

thread-loader ,在worker 池(worker pool)中运行加载器loader。把thread-loader 放置在其余 loader 以前, 放置在这个 thread-loader 以后的 loader 就会在一个单独的 worker 池(worker pool)中运行。

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        include: path.resolve('src'),
        use: [
          {
              loader: "thread-loader",
              // 有一样配置的 loader 会共享一个 worker 池(worker pool)
              options: {
                  // 产生的 worker 的数量,默认是 cpu 的核心数
                  workers: 2,

                  // 一个 worker 进程中并行执行工做的数量
                  // 默认为 20
                  workerParallelJobs: 50,

                  // 额外的 node.js 参数
                  workerNodeArgs: ['--max-old-space-size', '1024'],

                  // 闲置时定时删除 worker 进程
                  // 默认为 500ms
                  // 能够设置为无穷大, 这样在监视模式(--watch)下能够保持 worker 持续存在
                  poolTimeout: 2000,

                  // 池(pool)分配给 worker 的工做数量
                  // 默认为 200
                  // 下降这个数值会下降整体的效率,可是会提高工做分布更均一
                  poolParallelJobs: 50,

                  // 池(pool)的名称
                  // 能够修更名称来建立其他选项都同样的池(pool)
                  name: "my-pool"
              }
          }, 
          {
              loader:'babel-loader'
          }
        ]
      }
    ]
  }
}

一样,thread-loader也不是越多越好,也请只在耗时的loader 上使用。

2.3.3 webpack-parallel-uglify-plugin

Webpack3 中,咱们通常使用 UglifyJS 来压缩代码,可是这个是单线程运行的,也就是说多个js文件须要被压缩,它须要一个个文件进行压缩。因此说在正式环境打包压缩代码速度很是慢(由于压缩JS代码须要先把代码解析成AST语法树,再去应用各类规则分析和处理AST,致使这个过程耗时很是大)。为了加快效率,咱们可使用 webpack-parallel-uglify-plugin 插件,该插件会开启多个子进程,把对多个文件压缩的工做分别给多个子进程去完成,可是每一个子进程仍是经过UglifyJS去压缩代码。无非就是变成了并行处理该压缩了,并行处理多个子任务,提升打包效率。来并行运行 UglifyJS,从而提升效率。

Webpack4 中,咱们就不须要以上这些操做了,只须要将 mode 设置为 production 就能够默认开启以上功能。代码压缩也是咱们必作的性能优化方案,固然咱们不止能够压缩JS 代码,还能够压缩HTMLCSS 代码,而且在压缩 JS 代码的过程当中,咱们还能够经过配置实现好比删除 console.log 这类代码的功能。

let ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
module.exports = {
    module: {},
    plugins: [
        new ParallelUglifyPlugin({
            workerCount:3,//开启几个子进程去并发的执行压缩。默认是当前运行电脑的cPU核数减去1
            uglifyJs:{
                output:{
                    beautify:false,//不须要格式化
                    comments:false,//不保留注释
                },
                compress:{
                    warnings:false,//在Uglify]s除没有用到的代码时不输出警告
                    drop_console:true,//删除全部的console语句,能够兼容ie浏览器
                    collapse_vars:true,//内嵌定义了可是只用到一次的变量
                    reduce_vars:true,//取出出现屡次可是没有定义成变量去引用的静态值
                }
            },
        })
    ]
}

关于该插件更加详细的用法请参照这里webpack-parallel-uglify-plugin

2.4 DllPlugin&DllReferencePlugin

DllPlugin能够将特定的类库提早打包成动态连接库,在一个动态连接库中能够包含给其余模块调用的函数和数据,把基础模块独立出来打包到单独的动态链接库里,当须要导入的模块在动态链接库里的时候,模块不用再次被打包,而是去动态链接库里获取。这种方式能够极大的减小打包类库的次数,只有当类库更新版本才有须要从新打包,而且也实现了将公共代码抽离成单独文件的优化方案。

这里咱们能够先将reactreact-dom单独打包成动态连接库,首先新建一个新的webpack配置文件:webpack.dll.js

const path = require('path');
const DllPlugin = require('webpack/lib/DllPlugin');
module.exports = {
    // 想统一打包的类库
    entry:['react','react-dom'],
    output:{
        filename: '[name].dll.js',  //输出的动态连接库的文件名称,[name] 表明当前动态连接库的名称
        path:path.resolve(__dirname,'dll'),  // 输出的文件都放到 dll 目录下
        library: '_dll_[name]',//存放动态连接库的全局变量名称,例如对应 react 来讲就是 _dll_react
    },
    plugins:[
        new DllPlugin({
            // 动态连接库的全局变量名称,须要和 output.library 中保持一致
            // 该字段的值也就是输出的 manifest.json 文件 中 name 字段的值
            // 例如 react.manifest.json 中就有 "name": "_dll_react"
            name: '_dll_[name]',
            // 描述动态连接库的 manifest.json 文件输出时的文件名称
            path: path.join(__dirname, 'dll', '[name].manifest.json')
        })
    ]
}

而后咱们须要执行这个配置文件生成依赖文件:

webpack --config webpack.dll.js --mode development

接下来咱们须要使用 DllReferencePlugin 将依赖文件引入项目中

const DllReferencePlugin = require('webpack/lib/DllReferencePlugin')
module.exports = {
  // ...省略其余配置
  plugins: [
    new DllReferencePlugin({
      // manifest 就是以前打包出来的 json 文件
      manifest:path.join(__dirname, 'dll', 'react.manifest.json')
    })
  ]
}

2.5 noParse

module.noParse 属性,能够用于配置那些模块文件的内容不须要进行解析(即无依赖) 的第三方大型类库(例如jquery,lodash)等,使用该属性让 Webpack不扫描该文件,以提升总体的构建速度。

module.exports = {
    module: {
      noParse: /jquery|lodash/, // 正则表达式
      // 或者使用函数
      noParse(content) {
        return /jquery|lodash/.test(content)
      }
    }
}

2.6 IgnorePlugin

IgnorePlugin用于忽略某些特定的模块,让webpack 不把这些指定的模块打包进去。

module.exports = {
  // ...省略其余配置
  plugins: [
    new webpack.IgnorePlugin(/^\.\/locale/,/moment$/)
  ]
}

webpack.IgnorePlugin()参数中第一个参数是匹配引入模块路径的正则表达式,第二个参数是匹配模块的对应上下文,即所在目录名。

2.7 打包文件分析工具

webpack-bundle-analyzer插件的功能是能够生成代码分析报告,帮助提高代码质量和网站性能。

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports={
      plugins: [
          new BundleAnalyzerPlugin({
            generateStatsFile: true, // 是否生成stats.json文件
          })  
        // 默认配置的具体配置项
        // new BundleAnalyzerPlugin({
        //   analyzerMode: 'server',
        //   analyzerHost: '127.0.0.1',
        //   analyzerPort: '8888',
        //   reportFilename: 'report.html',
        //   defaultSizes: 'parsed',
        //   openAnalyzer: true,
        //   generateStatsFile: false,
        //   statsFilename: 'stats.json', 
        //   statsOptions: null,
        //   excludeAssets: null,
        //   logLevel: info
        // })
  ]
}

使用方式:

"generateAnalyzFile": "webpack --profile --json > stats.json", // 生成分析文件
"analyz": "webpack-bundle-analyzer --port 8888 ./dist/stats.json" // 启动展现打包报告的http服务器

2.8 费时分析

speed-measure-webpack-plugin,打包速度测量插件。这个插件能够测量webpack构建速度,能够测量打包过程当中每一步所消耗的时间,而后让咱们能够有针对的去优化代码。

const SpeedMeasureWebpackPlugin = require('speed-measure-webpack-plugin');
const smw = new SpeedMeasureWebpackPlugin();
// 用smw.wrap()包裹webpack的全部配置项
module.exports =smw.wrap({
    module: {},
    plugins: []
});

2.9 一些小的优化点

咱们还能够经过一些小的优化点来加快打包速度

  • resolve.extensions:用来代表文件后缀列表,默认查找顺序是 ['.js', '.json'],若是你的导入文件没有添加后缀就会按照这个顺序查找文件。咱们应该尽量减小后缀列表长度,而后将出现频率高的后缀排在前面
  • resolve.alias:能够经过别名的方式来映射一个路径,能让 Webpack 更快找到路径
module.exports ={
    // ...省略其余配置
    resolve: {
        extensions: [".js",".jsx",".json",".css"],
        alias:{
            "jquery":jquery
        }
    }
};

3. 减小 Webpack 打包后的文件体积

3.1 对图片进行压缩和优化

image-webpack-loader这个loder能够帮助咱们对打包后的图片进行压缩和优化,例如下降图片分辨率,压缩图片体积等。

module.exports ={
    // ...省略其余配置
    module: {
        rules: [
            {
                test: /\.(png|svg|jpg|gif|jpeg|ico)$/,
                use: [
                    'file-loader',
                    {
                        loader: 'image-webpack-loader',
                        options: {
                            mozjpeg: {
                                progressive: true,
                                quality: 65
                            },
                            optipng: {
                                enabled: false,
                            },
                            pngquant: {
                                quality: '65-90',
                                speed: 4
                            },
                            gifsicle: {
                                interlaced: false,
                            },
                            webp: {
                                quality: 75
                            }
                        }
                    }
                ]
            }
        ]
    }
};

3.2 删除无用的CSS样式

有时候一些时间久远的项目,可能会存在一些CSS样式被迭代废弃,须要将其剔除掉,此时就可使用purgecss-webpack-plugin插件,该插件能够去除未使用的CSS,通常与 globglob-all 配合使用。

注意:此插件必须和CSS代码抽离插件mini-css-extract-plugin配合使用。

例如咱们有样式文件style.css

body{
    background: red
}
.class1{
    background: red
}

这里的.class1显然是无用的,咱们能够搜索src目录下的文件,删除无用的样式。

const glob = require('glob');
const PurgecssPlugin = require('purgecss-webpack-plugin');

module.exports ={
    // ...
    plugins: [
        // 须要配合mini-css-extract-plugin插件
        new PurgecssPlugin({
            paths: glob.sync(`${path.join(__dirname, 'src')}/**/*`, 
                  {nodir: true}), // 不匹配目录,只匹配文件
            })
        }),
    ]
}

3.3 以CDN方式加载资源

咱们知道,通常经常使用的类库都会发布在CDN上,所以,咱们能够在项目中以CDN的方式加载资源,这样咱们就不用对资源进行打包,能够大大减小打包后的文件体积。

CDN方式加载资源须要使用到add-asset-html-cdn-webpack-plugin插件。咱们以CDN方式加载jquery为例:

const AddAssetHtmlCdnPlugin = require('add-asset-html-cdn-webpack-plugin')

module.exports ={
    // ...
    plugins: [
        new AddAssetHtmlCdnPlugin(true,{
            'jquery':'https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js'
        })
    ],
    //在配置文件中标注jquery是外部的,这样打包时就不会将jquery进行打包了
    externals:{
      'jquery':'$'
    }
}

3.4 开启Tree Shaking

Tree-shaking,摇晃树。顾名思义就是当咱们摇晃树的时候,树上干枯的没用的叶子就会掉下来。类比到咱们的代码中就是将没用的代码摇晃下来,从而实现删除代码中未被引用的代码。

这个功能在webpack4中,当咱们将mode设置为production时,会自动进行tree-shaking

来看下面代码:

main.js

import { minus } from "./calc";
console.log(minus(1,1));

calc.js

import {test} from './test';
export const sum = (a, b) => {
  return a + b + 'sum';
};
export const minus = (a, b) => {
  return a - b + 'minus';
};

test.js

export const test = ()=>{
    console.log('hello')
}
console.log(test());

观察上述代码其实咱们主要使用minus方法,test.js代码是有反作用的!所谓"反作用",官方文档以下解释:

「反作用」的定义是,在导入时会执行特殊行为的代码,而不是仅仅暴露一个 export 或多个 export。举例说明,例如 polyfill,它影响全局做用域,而且一般不提供 export。

对上述代码进行打包后发现'hello'依然会被打印出来,这时候咱们须要在package.json中配置配置不使用反作用:

{
  "sideEffects": false
}

若是这样设置,默认就不会导入css文件啦,由于咱们引入css也是经过import './style.css'

这里重点就来了,tree-shaking主要针对es6模块,咱们可使用require语法导入css,可是这样用起来有点格格不入,因此咱们能够配置css文件不是反作用,以下:

{
    "sideEffects":[
        "**/*.css"
    ]
}

3.5 开启Scope Hoisting

Scope Hoisting 可让 Webpack 打包出来的代码文件更小、运行的更快, 它又译做 "做用域提高",是在 Webpack3 中新推出的功能。

因为最初的webpack转换后的模块会包裹上一层函数,import会转换成require,由于函数会产生大量的做用域,运行时建立的函数做用域越多,内存开销越大。而Scope Hoisting 会分析出模块之间的依赖关系,尽量的把打包出来的模块合并到一个函数中去,而后适当地重命名一些变量以防止命名冲突。这个功能在webpack4中,当咱们将mode设置为production时会自动开启。

好比咱们但愿打包两个文件

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),这样的打包方式生成的代码明显比以前的少多了,而且减小多个函数后内存占用也将减小。若是你但愿在开发模式development中开启这个功能,只须要使用插件 webpack.optimize.ModuleConcatenationPlugin() 就能够了。

module.exports = {
  // ...
  plugins: [
    // 开启 Scope Hoisting
    new webpack.optimize.ModuleConcatenationPlugin(),
  ]
}

3.6 按需加载&动态加载

必你们在开发单页面应用项目的时候,项目中都会存在十几甚至更多的路由页面。若是咱们将这些页面所有打包进一个文件的话,虽然将多个请求合并了,可是一样也加载了不少并不须要的代码,耗费了更长的时间。那么为了首页能更快地呈现给用户,咱们确定是但愿首页能加载的文件体积越小越好,这时候咱们就可使用按需加载,将每一个路由页面单独打包为一个文件。在给单页应用作按需加载优化时,通常采用如下原则:

  • 对网站功能进行划分,每一类一个chunk
  • 对于首次打开页面须要的功能直接加载,尽快展现给用户,某些依赖大量代码的功能点能够按需加载
  • 被分割出去的代码须要一个按需加载的时机

动态加载目前并无原生支持,须要babel的插件:plugin-syntax-dynamic-import。安装此插件而且在.babelrc中配置:

{
  // 添加
  "plugins": ["transform-vue-jsx", "transform-runtime"],
  
}

例如以下示例:

index.js

let btn = document.createElement('button');
btn.innerHTML = '点击加载视频';
btn.addEventListener('click',()=>{
    import(/* webpackChunkName: "video" */'./video').then(res=>{
        console.log(res.default);
    });
});
document.body.appendChild(btn);

webpack.config.js

module.exports = {
    // ...
    output:{
      chunkFilename:'[name].min.js'
    }
}

这样打包后的结果最终的文件就是 video.min.js,而且刚启动项目时不会加载该文件,只有当用户点击了按钮时才会动态加载该文件。

4. 总结

以上就是一些经常使用的webpack优化手段,固然webpack优化手段还有不少,而且用法也有不少。须要的话能够阅读官方文档来深刻学习。

(完)

相关文章
相关标签/搜索