# webpack性能优化

# HMR

HMR: hot module replacement 热模块替换

作用:一个模块发生变化,只会重新发包这有个模块(而不是打包所有模块),极大提升构建速度

# 开启HMR功能

devServer: {
    contentBase: resolve(__dirname,'build'),
    compress: true,
    port: 3000,
    // 开启HMR功能
    open: true
}

css文件:可以使用HMR,因为style-loader内部实现了

js文件:默认不能使用HMR

注意:HMR功能对js的处理,只能处理非入口js文件的其他文件

if(module.hot) {
    // 一旦 module.hot为true,说明开启了HMR功能.   让HMR功能代码生效
    module.hot.accept('./print.js',function() {
        // 方法会监听 print.js 文件的变化, 一旦发生变化,其他模块不会重新打包构建
        // 会执行后面的回调函数
        print()
    })
}

html文件:默认不能使用HMR功能,同时会导致问题: html文件不能热更新了,解决办法 修改entry入口,将html文件引入 entry: ['./src/js/index.js','./src/index.html']

# SourceMap

source-map: 一种提供源代码到构建代码映射的技术(如果构建代码出错了,通过映射可以追踪代码错误)

[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map

内联 和 外联 的区别:1. 外部生产了文件,内联没有 2. 内联构建速度更快

  • source-map: 外联
    • 错误代码准确信息 和 源代码的错误位置
  • inline-source-map: 内联 --> 只生成一个内联sourceMap
    • 错误代码准确信息 和 源代码的错误位置
  • hidden-source-map: 外联
    • 不能追踪源代码错误,只能提示到构建后代码的错误位置
  • eval-source-map: 内联 --> 每一个文件都生产sourceMap,都在eval函数中
    • 错误代码准确信息 和 源代码的错误位置
  • nosources-source-map: 外联
    • 错误代码准确信息,但是没有任何源代码信息
  • cheap-source-map: 外联
    • 错误代码准确信息,但是没有任何源代码信息
    • 只能精确到行,不能精确到列
  • cheap-module-source-map: 外联
    • 错误代码准确信息 和 源代码的错误位置

# 环境选择

# 开发环境

开发环境:速度快,调试更友好
	速度快(eval > inline > cheap > ....)
		eval-cheap-source-map
		eval-source-map
	调试更友好
    	source-map
    	cheap-module-source-map
    	cheap-source-map
    eval-source-map || eval-cheap-module-source-map
    vue react脚手架默认使用的是eval-source-map

# 生产环境

生产环境:源代码要不要隐藏? 调试要不要更友好
	隐藏代码
		nosources-source-map
		hidden-source-map
	调试
		source-map
		cheap-module-source-map

# cache

# babel缓存

cacheDirectory: true

{
    test: /\.js$/,
    exclude: /node_modules/, // 排除
    loader: 'babel-loader',
    options: {
		// 预设:指示babel做怎样的兼容处理
       	presets: [
            '@babel/preset-env',
            {
                // 按需加载
                useBuiltIns: 'usage',
                // 指定core-js版本
                corejs: {
                    version: 3
                },
                // 指定兼容性做到哪个版本浏览器
                targets: {
                    chrome: '60',
                    firefox: '60',
                    ie: '9',
                    safari: '10',
                    edge: '17'
                }
            }
        ],
        // 开启babel缓存
        cacheDirectory: true
    }
},

# 文件缓存

# hash:每次webpack构建时会生成的唯一的hash值

output: {
    filename: 'js/built.[hash:10].js',
    path: resolve(__dirname, 'build')
},
    
new MiniCssExtractPlugin({
    filename: 'css/built.[hash:10].css'
}),

问题:因为js css 同时使用一个hash值,如果重新打包,会导致所有缓存失效

# chunkhash

根据chunk生成的hash值,如果打包来源于同一个chunk,那么它的hash值就一样

问题:js和css的hash值还是一样的,因为css是在js中被引入的,所以同属于一个chunk

# contenthash

根据文件的内容生成hash值,不同的文件hash一定会不一样

# 总结

babel缓存 让第二次打包构建速度更快,文件缓存 让代码上线运行缓存更好用

# tree shaking

tree-shaking: 去除无用代码

前提:1. 必须使用es6模块化 2. 开启production环境

作用: 减少代码体积

在package.json中配置 "sideEffects": false 所有代码都没有副作用(都可以进行tree shaking)

问题: 可以会把css / @babel/polyfill (副作用)文件干掉

"sideEffects": ["*.css"]

# code split

# lazy loading

# 如何优化

我们将index.js中的异步代码打开,并且注释掉同步代码,然后使用打包命令npm run start,成功打包后我们使用开发者工具中的Network,可以看出页面加载时已经同时加载了打包相关的代码:

png

我们不妨使用一个监听点击的函数来决定页面中DOM的挂载,所以我们index.js写成这样:

// 异步代码输出

function getComponent() {
    return import( /* webpackChunkName: "lodash" */ 'lodash').then(({
        default: _
    }) => {
        var dom = document.createElement('div');
        dom.innerHTML = _.join(['Copy', 'Splitting'], ' ');
        return dom;
    })
}

// 使用点击事件触发挂载DOM节点
document.addEventListener('click', () => {
    getComponent().then(dom => {
        document.body.appendChild(dom);
    })
})

此时我们再去运行npm run start,成功打包后我们发现bundle打包文件夹中依然还是那三个文件,此时我们打开页面去看Network,发现我们未点击页面之前,只加载了一个打包文件和静态页面index.html,而第三方插件lodash并没有载入进来:

png

此时只有我们触发点击事件时,才会去调用 getComponent() 的方法,也才会去执行lodash的异步导入,此时页面才回去载入 vendors~lodash.js 这个打包文件,这样也就形成了一个Lazy Loading懒加载模式,这相比与同步代码时直接引入的方式更有优化的地方~

# 其他场景

如果使用过Vue/React这些框架的话,里面都是有路由跳转的概念,就好比一个SPA单页面应用,有首页模块、推荐页模块、购物车模块、个人中心模块等,此时我们把整个应用的代码打包上线而不去做分割时,那用户载入我们首页的时候,是不是都要加载我们这个应用逻辑压缩代码?这样肯定是不行的,所以就有了路由跳转的概念,只有我们切换到那个模块时才会去加载对应的逻辑代码,要实现这个肯定少不了在路由中进行异步加载的配置,然后配合webpack进行识别自动代码分割,这也算是Lazy Loading懒加载吧

那Lazy Loading懒加载是webpack里的一个概念吗?

其实并不是的,Lazy Loading懒加载是ES语法中提出的概念,而webpack只是能识别这种Import的语法进行相应的代码分割而已

# 结语

其实在webpack中关于Lazy Loading懒加载其实很常见也很实用,后面会不断实践,你会发现懒加载无处不在~

# 多进程打包

  1. 需要先下载thread-loader npm i thread-loader -D
  2. 一般在babel-loader下配置
  3. 代码
{
    test: /\.js$/,
    exclude: /node_modules/, // 排除
    // loader: 'babel-loader',
    use: [
        /**
        * 开启多进程打包
        * 进程开启大概为600ms,进程通信也有开销
        * 只有工作消耗时间比较长,才需要多进程打包
        *
        */
        'thread-loader',
        {
            loader: 'babel-loader',
            options: {
                // 预设:指示babel做怎样的兼容处理
                presets: [
                    '@babel/preset-env',
                    {
                        // 按需加载
                        useBuiltIns: 'usage',
                        // 指定core-js版本
                        corejs: {
                            version: 3
                        },
                        // 指定兼容性做到哪个版本浏览器
                        targets: {
                            chrome: '60',
                            firefox: '60',
                            ie: '9',
                            safari: '10',
                            edge: '17'
                        }
                    }
                ],
                // 开启babel缓存
                cacheDirectory: true
            }
        }
    ],
}

# externals

externals 排除一下包来进行打包

externals: {
    // 忽略库名 --- 对应npm包名
    jquery: 'jQuery'
}

# dll

使用dll技术,对某些库(第三方:jquery,vue,react)进行单独打包

  1. 创建webpack.dll.js
/**
 * 当你运行 webpack 时, 默认查找 webpack.config.js 配置文件
 * 需要运行webpack.dll.js 文件的话 指令为  webpack --config webpack.dll.js
 */
const { resolve } = require('path');
const webpack = require('webpack');

module.exports = {
    entry: {
        // 最终打包生产的name --> jquery
        // ['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'
};
2. 在`webpack.config.js配置`
plugins: [
    // 告诉webpack哪些库不参与打包,同时使用时的名称也得变~
    new webpack.DllReferencePlugin({
        manifest: resolve(__dirname,'dll/manifest.json')
    })
]
3. `cnpm i add-asset-html-webpack-plugin -D`
// 将某个文件打包输出出去,并在html中通过自动引入该文件
new AddAssetHtmlWebpackplugin({
    filepath: resolve(__dirname,'dll/jquery.js')
})