前言

如何实现webpack打包优化,主要有以下两个优化点:

  • 如何减少打包时间
  • 如何减少打包大小

减少打包时间

优化Loader

对于Loader来说,首先优化的当然是babel了,babel会将代码转成字符串并生成AST,然后继续转化成新的代码,转换的代码越多,效率就越低。

优化Loader的搜索范围

module.exports = {
    module: {
        rules: [
            test: /\.js$/, // 对js文件使用babel
            loader: 'babel-loader',
            include: [resolve('src')],// 只在src文件夹下查找
            // 不去查找的文件夹路径,node_modules下的代码是编译过得,没必要再去处理一遍
            exclude: /node_modules/ 
        ]
    }
}
缓存已编译过的文件

另外可以将babel编译过文件缓存起来,以此加快打包时间,主要在于设置cacheDirectory

loader: 'babel-loader?cacheDirectory=true'

HappyPack

因为受限于Node的单线程运行,所以webpack的打包也是单线程的,使用HappyPack可以将Loader的同步执行转为并行,从而执行Loader时的编译等待时间。


module: {
    loaders: [
        test: /\.js$/,
        include: [resolve('src')],
        exclude: /node_modules/,
        loader: 'happypack/loader?id=happybabel' //id对应插件下的配置的id
    ]
},
plugins: [
    new HappyPack({
        id: 'happybabel',
        loaders: ['babel-loader?cacheDirectory'],
        threads: 4, // 线程开启数
    })
]

DllPlugin

该插件可以将特定的类库提前打包然后引入,这种方式可以极大的减少类库的打包次数,只有当类库有更新版本时才会重新打包,并且也实现了将公共代码抽离成单独文件的优化方案


// webpack.dll.conf.js
const path = require('path')
const webpack = require('webpack')
module.exports = {
    entry: {
        vendor: ['react'] // 需要统一打包的类库
    }output: {
      path: path.join(__dirname, 'dist'),
        filename: '[name].dll.js',
        library: '[name]-[hash]'
    },
    plugins: [
        new webpack.DllPlugin({
            name: '[name]-[hash]', //name必须要和output.library一致
            context: __dirname, //注意与DllReferencePlugin的context匹配一致
            path: path.join(__dirname, 'dist', '[name]-manifest.json')
        })
    ]
}

然后在package.json文件中增加一个脚本


'dll': 'webpack --config webpack.dll.js --mode=development'
//运行后会打包出react.dll.js和manifest.json两个依赖文件

最后使用DllReferencePlugin将刚生成的依赖文件引入项目中


// webpack.conf.js
module.exports = {
    //...其他配置
    plugins: [
        new webpack.DllReferencePlugin({
            context: __dirname,
            manifest: require('./dist/vendor-manifest.json') //此即打包出来的json文件
        })
    ]
}

更多关于webpack配置DllPlugin请参考如何使用 Webpack 的 Dllplugin和webpack中DllPlugin用法

代码压缩相关

  • 启用gzip压缩
  • webpack3中,可以使用UglifyJS压缩代码,但是它是单线程的,因此可以使用webpack-parallel-uglify-plugin来运行UglifyJS,但在webpack4中只要启动了mode为production就默认开启了该配置
  • 压缩html和css代码,通过配置删除console.log和debugger等,防止可能造成的内存泄漏
new UglifyJsPlugin({
    UglifyOptions: {
        compress: {
            warnings: false,
            drop_console: true,
            pure_funcs: ['console.log']
        }
    },
    sourceMap: config.build.productionSourceMap,
    parallel: true
})
//或使用以下配置
new webpack.optimize.UglifyJsPlugin({
    compress: {
        warnings: false,
        drop_debugger: true,
        drop_console: true
    }
})

减少包大小

按需加载

首页加载文件越小越好,将每个页面单独打包为一个文件,(同样对于loadsh类库也可以使用按需加载),原理即是使用的时候再去下载对应的文件,返回一个promise,当promise成功后再去执行回调。

Scope Hoisting

它会分析出模块之前的关系,尽可能的把打包出来的模块合并到一个函数中去


// 如在index.js问价中引用了test.js文件
export const a = 1  // test.js
import {a} from './test.js'  // index.js
// 以上打包出来的文件会有两个函数,类似如下
[
    function(module, exports, require) {} // **0**
    function(module, exports, require) {} // **1**
]

如果使用scope hoisting的话会尽量打包成一个函数,在webpack 5中只需开启concatenateModules即可


module.exports = {
    optimize: {
        concatenateModules: true
    }
}

Tree shaking

它会把js文件中无用的模块或者代码删掉。如:在打包时会移除掉javascript上下文中无用的代码,从而优化打包的结果。
在webpack5中已经自带tree-shaking功能,在打包模式为production时,默认开启 tree-shaking功能
下面通过代码来看下效果吧:


// a.js
export default {
  a: 1
}
// index.js
import a from './a'
// 没有使用a变量
console.log('hello world');

或者


import './a'
// 直接导入
console.log('hello world');

发现导出的代码,竟然把a.js中的所有代码都删除了

(()=>{"use strict";console.log("hello world")})();

tree shaking自动帮我们把无用的代码自动删除了。但是webpack是怎么知道哪些代码是没有使用的,是通过哪些规则来处理的,带着这些疑问我们继续往下看:

// a.js
function a () {
  console.log('a')
}

function b () {
  console.log('b')
}

export default {
  a, b
}
// index.js
import a from './a'
// 使用a变量
console.log(a.a())
console.log('hello world');

发现导出的代码:a.js里面的部分代码被删除掉了。删除了没有使用到的b函数,正确的保留了a函数。注意webpack4是做不到这一点的,只有webpack5才又这个功能。webpack 4 没有分析模块的导出和引用之间的依赖关系。webpack 5 有一个新的选项 optimization.innerGraph,在生产模式下是默认启用的,它可以对模块中的标志进行分析,找出导出和引用之间的依赖关系。


(()=>{"use strict";const o=function(){console.log("a")};console.log(o()),console.log("hello world")})();

webpack是怎么去判断哪些代码是无用的呢?
其实就是 Webpack 发现你并没有代码。Webpack 跟踪整个应用程序的 import/export 语句,因此,如果它看到导入的东西最终没有被使用,它就认定这部分为“死代码”,并会对其进行 tree-shaking
判断“死代码”并不总是那么明确的。下面是一些“死代码”和“活代码”的例子:


// 导入并赋值给 JavaScript 对象,然后在下面的代码中被用到
// 这会被看作“活”代码,不会做 tree-shaking
import Stuff from './stuff';
doSomething(Stuff);

// 导入并赋值给 JavaScript 对象,但在接下来的代码里没有用到
// 这就会被当做“死”代码,会被 tree-shaking
import Stuff from './stuff';
doSomething();

// 导入但没有赋值给 JavaScript 对象,也没有在代码里用到
// 这会被当做“死”代码,会被 tree-shaking
import './stuff';
doSomething();

// 导入整个库,但是没有赋值给 JavaScript 对象,也没有在代码里用到
// 非常奇怪,这竟然被当做“活”代码,因为 Webpack 对库的导入和本地代码导入的处理方式不同。
import 'my-lib';
doSomething();

注意 Webpack 不能百分百安全地进行 tree-shaking。有些模块导入,只要被引入,就会对应用程序产生重要的影响。一个很好的例子就是全局样式表,或者设置全局配置的JavaScript 文件
Webpack 认为这样的文件有“副作用”。具有副作用的文件不应该做 tree-shaking,因为这将破坏整个应用程序。Webpack 的设计者清楚地认识到不知道哪些文件有副作用的情况下打包代码的风险,因此webpack4默认地将所有代码视为有副作用。这可以保护你免于删除必要的文件,但这意味着 Webpack 的默认行为实际上是不进行 tree-shaking。值得注意的是webpack5默认会进行 tree-shaking。
package.json 有一个特殊的属性 sideEffects,可以通过这个属性来判断你的代码是否有副作用:

  • true 如果不指定其他值的话。这意味着所有的文件都有副作用,也就是没有一个文件可以 tree-shaking

  • false 告诉 Webpack 没有文件有副作用,所有文件都可以 tree-shaking

  • 第三个值 […] 是文件路径数组。它告诉 webpack,除了数组中包含的文件外,你的任何文件都没有副作用。因此,除了指定的文件之外,其他文件都可以安全地进行 tree-shaking


// 所有文件都有副作用,全都不可 tree-shaking
{
 "sideEffects": true
}
// 没有文件有副作用,全都可以 tree-shaking
{
 "sideEffects": false
}
// 只有这些文件有副作用,所有其他文件都可以 tree-shaking,但会保留这些文件
{
 "sideEffects": [
  "./src/file1.js",
  "./src/file2.js"
 ]
}

webpack4 没有对 CommonJs 导出和 require() 调用时的导出使用分析。webpack 5 增加了对一些 CommonJs 构造的支持,允许消除未使用的 CommonJs 导出,并从 require() 调用中跟踪引用的导出名称。
所以这边也比较建议大家升级使用webpack5。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐