Webpack 性能优化

1,204 阅读11分钟

Webpack性能优化.png

一、 开发优化(development)

1. 文件监听

文件监听是在发现源码发生变化时,自动重新构建出新的输出文件,会更新到磁盘上, 轮询判断⽂件的最后编辑时间是否变化,某个⽂件发⽣了变化,并不会⽴刻告诉监听者,先缓存起来

缺点: 需要手动刷新

启动方式:

  1. 方式1

启动 webpack 命令式 带上--watch 参数,启动监听

// package.json
"scripts": {
  "watch":"webpack --watch"
}
npm run watch

npx webpack --watch
  1. 方法式2

在配置⽂件⾥设置 watch:true

// webpack.config.js
module.exports = {
    watch: true, //默认false,不开启
        //配合watch,只有开启才有作⽤
    watchOptions: {
        //默认为空,不监听的⽂件或者⽬录,⽀持正则
        ignored: /node_modules/,
        //监听到⽂件变化后,等300ms再去执⾏,默认300ms,
        aggregateTimeout: 300,
        //判断⽂件是否发⽣变化是通过不停的询问系统指定⽂件有没有变化,默认每秒问1次
        poll: 1000 //ms
    }
    // 轮询 1s查看1次
}

2. webpack-dev-server 自动打开浏览器,保存自动刷新

2.1 安装

注意: webpack-dev-server 和 webpack-cli 4.x.x有兼容性问题,会报错, webpack-cli 需要安装 3.x.x,

npm install webpack-dev-server -D

启动

// package.json
"scripts": {
   "server": "webpack-dev-server"
}
npm run server

2.2 配置

// webpack.config.js
var path = require('path');

module.exports = { 
    //...
    devServer: {
        contentBase: path.join(__dirname, 'dist'),
        compress: true,
        port: 9000 
    }
};
  1. devServer: 打包的模块是放在内存中的,从而提升打包速度.

  2. contentBase:告诉服务器内容的来源。仅在需要提供静态文件时才进行配置.

  3. port: 端口

  4. open: 是否打开浏览器

  5. proxy: 解决开发环境的跨域问题

// webpack.config.js
devserver: {
	proxy: {
    	
  		'/api': { // 请求
     	target: 'http://localhost:9092'
  		}
	}
}
// index.js
axios.get("/api/info").then(res => {
   console.log(res);
});
  1. mock server

before 和 after 是中间件 提供的两个钩子, 一个在中间件启动之前,一个在中间件启动之后

before(app, server) {
  app.get('/api/mock', (req, res) => {
      res.json({
          hello: 'express'
      })
  })
}

3. Hot Module Replacement (HMR:热模块替换)

3.1 热更新配置

热更新: 新代码生效, 网页不刷新, 状态不丢失

// 引入 webpack
const webpack = require('webpack');

module.exports = {
    plugins: [
    // 使用 webpack 插件
        new webpack.HotModuleReplacementPlugin(),
    ],
    devServer: {
      hot: true,
      //即便HMR不⽣效,浏览器也不要⾃动刷新,就开启hotOnly
      hotOnly: true,
    }
} 

注意:CSS 热更新 不⽀持抽离出的 CSS, 不建议开发环境使用 mini-css-extract-plugin,还有不⽀持contenthash,chunkhash

3.2 JS 热模块替换

需要使⽤ module.hot.accept 来观察模块更新 从⽽更新

//number.js
function number() {
	var div = document.createElement("div");
	div.setAttribute("id", "number");
	div.innerHTML = 13000;
	document.body.appendChild(div);
}
export default number;
import number from "./number";

number();

// 是否开启热更新
if (module.hot) {
    // 模块是否发生变化
    module.hot.accept("./number", function () {
        // 移除该元素
        document.body.removeChild(document.getElementById("number"));
        // 重新添加该元素
        number();
    });
}

3.3 React

React Hot Loader: 实时调整 react 组件。

3.4 Vue

Vue Loader: 此 loader 支持 vue 组件的 HMR,提供开箱即用体验

4. 缩小 Loader 处理范围

优化 loader 配置, 缩小 loader 处理范围

4.1 include: 只处理那

module.exports = {
	module: {
    	rules: [{
	    	test: /\.css$/,
            	// 只处理src 目录下的文件
    		include: path.resolve(__dirname, './src'),
		}]
    }
}

4.2 exclude: 不处理那

module.exports = {
    module: {
        rules: [{
            test: /\.css$/,
            // 不去处理那
            exclude: path.resolve(__dirname, './node_modules')
        }]
    }
}

include 和 exclude 二选一,推荐使用 include

5. 优化 resolve 配置

5.1 resolve.modules

resolve.modules ⽤于配置webpack去哪些⽬录下寻找第三⽅模块,默认是['node_modules']

寻找第三⽅模块,默认是在当前项⽬⽬录下的node_modules⾥⾯去找,如果没有找到,就会去上⼀级⽬录 ../node_modules 找,再没有会去 ../../node_modules 中找,以此类推,和Node.js的模块寻找机制 很类似。

// webpack.config.js
module.exports={
    resolve: {
        // 指定路径只去这里查找,没有就是没有了
        modules: [
            path.resolve(__dirname, "./node_modules")
        ]
    }
}

5.2 resolve.alias

resolve.alias 配置通过别名来将原导⼊路径映射成⼀个新的导⼊路径

默认情况下,webpack会从⼊⼝⽂件./node_modules/bin/react/index开始递归解析和处理依赖的⽂件。我们可以直接指定⽂件,避免这处的耗时。

// webpack.config.js
module.exports={
    resolve: {
        alias: {
    		"@": path.join(__dirname, "./pages"),
    		// 指定 react 文件路径,节约查找时间
            react: path.resolve(__dirname, "./node_modules/react/umd/react.production.min.js"),
    		// 指定 react-dom 文件路径,节约查找时间
    		"react-dom": path.resolve(__dirname,"./node_modules/react-dom/umd/react-dom.production.min.js")
    }
}

5.3 resolve.extensions

resolve.extensions 在导⼊语句没带⽂件后缀时,webpack会⾃动带上后缀后,去尝试查找⽂件是否存在。

  • 后缀尝试列表尽量的⼩
  • 导⼊语句尽量的带上后缀。
// webpack.config.js
module.exports={
    resolve: {
    	// 默认配置
    	extensions:['.js','.json','.jsx','.ts']
    }
}

6. 使⽤⼯具量化

6.1 speed-measure-webpack-plugin:可以测量各个插件和 loader 所花费的时间

安装:

npm i speed-measure-webpack-plugin -D
//webpack.config.js
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");

const smp = new SpeedMeasurePlugin();

const config = {
	//...webpack配置
}

module.exports = smp.wrap(config)

6.2 webpack-bundle-analyzer:分析webpack打包后的模块依赖关系

安装:

npm install webpack-bundle-analyzer -D

启动webpack 构建,会默认打开⼀个窗⼝

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
   //....
 	plugins: [
 	//...
 	new BundleAnalyzerPlugin(),
 	]
}

7. DllPlugin插件打包第三⽅类库 优化构建性能

项⽬中引⼊了很多第三⽅库,这些库在很⻓的⼀段时间内,基本不会更新,打包的时候分开打包来提升打包速度,⽽ DllPlugin 动态链接库插件,其原理就是把⽹⻚依赖的基础模块抽离出来打包到dll⽂件中,当需要导⼊的模块存在于某个dll中时,这个模块不再被打包,⽽是去dll中获取

Dll动态链接库 其实就是做缓存

webpack已经内置了对动态链接库的⽀持

  • DllPlugin: ⽤于打包出⼀个个单独的动态链接库⽂件
  • DllReferencePlugin:⽤于在主要的配置⽂件中引⼊DllPlugin插件打包好的动态链接库⽂件

新建webpack.dll.config.js⽂件,打包基础模块

我们在 index.js 中使⽤了第三⽅库 react 、 react-dom ,接下来,我们先对这两个库先进⾏打包。

7.1 打包出动态链接库

// webpack.dll.config.js
const path = require("path");
const { DllPlugin } = require("webpack");

module.exports = {
    mode: "development",
    entry: {
        react: ["react", "react-dom"] //! node_modules?
    },
    output: {
        path: path.resolve(__dirname, "./dll"),
        filename: "[name].dll.js",
        library: "react"
    },
    plugins: [
        new DllPlugin({
            // manifest.json⽂件的输出位置
            path: path.join(__dirname, "./dll", "[name]-manifest.json"),
            // 定义打包的公共vendor⽂件对外暴露的函数名
            name: "react"
        })
    ]
};

运行

npx webpack --config webpack.dll.config.js

会生成⼀个dll⽂件夹,⾥边有dll.js⽂件,这样我们就把我们的React这些已经单独打包了

  • dll⽂件包含了⼤量模块的代码,这些模块被存放在⼀个数组⾥。⽤数组的索引号为ID,通过变量讲⾃⼰暴露在全局中,就可以在 window.xxx访问到其中的模块
  • Manifest.json 描述了与其对应的dll.js包含了哪些模块,以及ID和路径。

7.2 引入动态连接库

  • 将⽹⻚依赖的基础模块抽离,打包到单独的动态链接库,⼀个动态链接库是可以包含多个模块的。
  • 当需要导⼊的模块存在于某个动态链接库中时,不要再次打包,直接使⽤构建好的动态链接库即可。
module.exports = {
	new DllReferencePlugin({
 	manifest: path.resolve(__dirname,"./dll/react-manifest.json")
	})
}
  • ⻚⾯依赖的所有动态链接库都需要被加载。
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initialscale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>webpack</title>
    <link href="css/main_e2bf39.css" rel="stylesheet" />
  </head>
  <body>
    <div id="app"></div>
    <!-- 引入动态连接库 -->
    <script type="text/javascript" src="react.dll.js"></script>
    <script type="text/javascript" src="js/main_142e6c.js"></script>
  </body>
</html>

使用 add-asset-html-webpack-plugin 插件,它会将我们打包后的 dll.js ⽂件注⼊到我们⽣成的 index.html 中.在webpack.base.config.js ⽂件中进⾏更改

安装: npm i add-asset-html-webpack-plugin

new AddAssetHtmlWebpackPlugin({
	filepath: path.resolve(__dirname, '../dll/react.dll.js') // 对应的 dll ⽂件路径
}),

8. HardSourceWebpackPlugin, DllPlugin 的替换方案

DllPlugin 这个理解起来不费劲,操作起来很费劲。然后出现了 HardSourceWebpackPlugin ,⼀样的优化效果,但是使⽤却及其简单

  • 提供中间缓存的作⽤
  • ⾸次构建没有太⼤的变化
  • 第⼆次构建时间就会有较⼤的节省

安装:

npm i hard-source-webpack-plugin -D
// webpack.config.js
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin')

module.exports = {
	plugins: [
    		new HardSourceWebpackPlugin()
    	]
}

9. IgnorePlugin 从 bundle 中排除某些模块

webpack 中文网

var webpack = require('webpack');

module.exports = {
  plugins: {
   new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
  }
}

10. 使⽤ happypack 并发执⾏任务, 实现多线程加速编译

运⾏在 Node.之上的Webpack是单线程模型的,也就是说Webpack需要⼀个⼀个地处理任务,不能同时处理多个任务。 Happy Pack 就能让Webpack做到这⼀点,它将任务分解给多个⼦线程去并发执⾏,⼦线程处理完后再将结果发送给主进程。从⽽发挥多核 CPU 电脑的威⼒。

happypack 启动也需要一定时间,按需使用。

安装:npm i -D happypack

const path = require('path');
const HappyPack = require('happypack');
const os = require('os');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });

module.exports = {
    mode: 'development',
    module: {
        rules: [{
            test: /\.css$/,
            include: path.resolve(__dirname, './src'),
            use: [
                {
                    // 一个 loader 对应一个 id
                    loader: 'happypack/loader?id=css'
                }
            ]
        }]
    },
    //在plugins中增加
    plugins: [
        new HappyPack({
             // ⽤唯⼀的标识符id,来代表当前的HappyPack是⽤来处理⼀类特定的⽂件
            id: "css",
            // 处理的 loader
            loaders: ['style-loader', 'css-loader'],
            threadPool: happyThreadPool
        })
    ]
};

11. ParallelUglifyPlugin

  • Webpack 内置 Uglify 工具压缩 JS 是单线程的
  • ParallelUglifyPlugin插件则会开启多个子线程,把对多个文件压缩的工作分别给多个子线程去完成,但是每个子进程还是通过UglifyJS去压缩代码

安装:

npm i -D webpack-parallel-uglify-plugin

配置:

const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');

module.exports = {
  plugins: {
    // 使用 ParallelUglifyPlugin 并行压缩输出的 JS 代码
    new ParallelUglifyPlugin({
      // 传递给 UglifyJS 的参数
      // (还是使用 UglifyJS 压缩, 只不过帮助开启多进程)
      uglifyJS: {
        output: {
          beautify: false, // 最紧凑的输出
          comments: false, // 删除所有的注释
        },
        compress: {
          // 删除所有的 `console.log`语句, 可以兼容 ie 浏览器
          drop_console: true,
          // 内嵌定义了但是只用到一次的变量
          collapse_vars: true,
          // 提取出出现多次但是没有定义成变量的静态值
          reduce_vars: true
        }
      }
    })
  }
}

二、生产优化(production)

1. 使⽤ externals 优化 cdn 静态资源

CDN通过将资源部署到世界各地,使得⽤户可以就近访问资源,加快访问速度。要接⼊CDN,需要把⽹⻚的静态资源上传到CDN服务上,在访问这些资源时,使⽤CDN服务提供的URL。

<body>
 	<div id="root">root</div>
 	<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
</body>

我们可以将⼀些JS⽂件存储在 CDN 上(减少 Webpack 打包出来的 js 体积),在 index.html 中通过标签引⼊

我们希望在使⽤时,仍然可以通过 import 的⽅式去引⽤(如 import $ from 'jquery' ),并且希望 webpack 不会对其进⾏打包,此时就可以配置 externals 。

//webpack.config.js
module.exports = {
    //...
    externals: {
        //jquery通过script引⼊之后,全局中即有了 jQuery 变量
        'jquery': 'jQuery'
    }
}

2. 使⽤静态资源路径 publicPath (CDN)

// webpack.config.js
output:{
    publicPath: '//cdnURL.com', //指定存放JS⽂件的CDN地址
}

确保静态资源⽂件的上传与否

3. css⽂件的处理

  • 使⽤postcss为样式⾃动补⻬浏览器前缀 caniuse
  • 借助 MiniCssExtractPlugin 抽离 css
  • 压缩 css

3.1 压缩 css

借助 ptimize-css-assets-webpack-plugin 和 cssnano

安装:

npm i optimize-css-assets-webpack-plugin -D
npm i cssnano -D

配置:

const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");

module.exports = {
    plugins: [
        new OptimizeCSSAssetsPlugin({
            cssProcessor: require("cssnano"), //引⼊cssnano配置压缩选项
            cssProcessorOptions: {
                discardComments: { removeAll: true }
            }
        })
    ]
}

4. 压缩 HTML

借助 htmlWebpackPlugin 插件

安装:

npm i html-webpack-plugin -D

配置:

new htmlWebpackPlugin({
    title: "京东商城",
    template: "./index.html",
    filename: "index.html",
    minify: {
        // 压缩HTML⽂件
        removeComments: true, // 移除HTML中的注释
        collapseWhitespace: true, // 删除空⽩符与换⾏符
        minifyCSS: true // 压缩内联css
    }
})

5. tree Shaking 清除⽆⽤ css, js (Dead Code)

Dead Code 特征:

  • 代码不会被执⾏,不可到达
  • 代码执⾏的结果不会被⽤到
  • 代码只会影响死变量(只写不读)

5.1 Css tree shaking

借助 purgecss-webpack-plugin 插件清除无用的 css

借助 glob-all 处理路径

安装:

npm i purgecss-webpack-plugin -D
npm i glob-all -D

配置:

const PurgeCSSPlugin = require('purgecss-webpack-plugin')
const globAll = require("glob-all");

module.exports = {
  plugins: [
      // 清除无用 css
      new PurgeCSSPlugin({
        paths: globAll.sync([
            // 要做 CSS Tree Shaking 的路径文件
            path.resolve(__dirname, './src/*.html'),
            // 请注意,我们同样需要对 html 文件进行tree shaking
            path.resolve(__dirname, './src/*.js')
        ])
      })
  ]
}

5.2 JavaScript tree shaking

生产模式自动开启,只⽀持 import ⽅式引⼊,不⽀持 commonjs 的⽅式引⼊

ES6 和 commonjs 的区别

commonjs 动态引入, 执行时引入,模块输出的是值的浅拷贝,模块输出后被改变,其他引用模块不会改变

ES6 静态引入, 编译时引入,模块输出的是值的引用,模块输出后被改变, 引用模块会改变

module.exports = {
    mode: 'production'
}
  • 自动开启代码压缩
  • Vue React 等会删除掉调式代码(如开发环境的 warning)
  • 自动启动 Tree-Shaking

6. 代码分割 code Splitting

单⻚⾯应⽤spa: 打包完后,所有⻚⾯只⽣成了⼀个bundle.js,代码体积变⼤,不利于下载,没有合理利⽤浏览器资源

多⻚⾯应⽤mpa: 如果多个⻚⾯引⼊了⼀些公共模块,那么可以把这些公共的模块抽离出来,单独打包。公共代码只需要下载⼀次就缓存起来了,避免了重复下载。

  • 代码体积更小
  • 创建函数作用域更少

code Splitting 概念与 webpack 并没有直接的关系,只不过 webpack 中提供了⼀种更加⽅便的⽅法供我们实现代码分割基于split-chunks-plugin

optimization: {
    splitChunks: {
        chunks: 'async',//对同步 initial,异步 async,所有的模块有效 all
        minSize: 30000,//最⼩尺⼨,当模块⼤于30kb
        maxSize: 0,//对模块进⾏⼆次分割时使⽤,不推荐使⽤
        minChunks: 1,//打包⽣成的chunk⽂件最少有⼏个chunk引⽤了这个模块
        maxAsyncRequests: 5,//最⼤异步请求数,默认5
        maxInitialRequests: 3,//最⼤初始化请求书,⼊⼝⽂件同步请求,默认3
        automaticNameDelimiter: '-',//打包分割符号
        name: true,//打包后的名称,除了布尔值,还可以接收⼀个函数function
        cacheGroups: {//缓存组
            vendors: {
            test: /[\\/]node_modules[\\/]/,
            name:"vendor", // 要缓存的 分隔出来的 chunk 名称
            priority: -10//缓存组优先级 数字越⼤,优先级越⾼
        },
        other:{
            chunks: "initial", // 必须三选⼀: "initial" | "all" | "async"(默认就是async)
            test: /react|lodash/, // 正则规则验证,如果符合就提取 chunk,
            name:"other",
            minSize: 30000,
            minChunks: 1,
        },
        default: {
            minChunks: 2,
            priority: -20,
            reuseExistingChunk: true//可设置是否重⽤该chunk
        }
        }
    }
}
optimization: {
 	//帮我们⾃动做代码分割
 	splitChunks:{
 		chunks:"all",//默认是⽀持异步,我们使⽤all
 	}
}

7. 作⽤域提升(Scope Hoisting)

webpack 通过 ES6 语法的静态分析,分析出模块之间的依赖关系,尽可能地把模块放到同⼀个函数中

示例:

// hello.js
export default "hello webpack";
import str from "./hello.js";
console.log(str);

Scope Hoisting 前

通过配置 optimization.concatenateModules=true 开启 Scope Hoisting

// webpack.config.js
module.exports = {
   optimization: {
   		concatenateModules: true
   }
}

Scope Hoisting后

三、development vs Production 模式区分打包

借助 npm install webpack-merge -D 合并公共配置

安装:

npm install webpack-merge -D
// webpack.dev.js
const merge = require("webpack-merge");
// 引入 Webpack 公共配置
const commonConfig = require("./webpack.common.js");
const devConfig = {
 ...
}

// 合并配置并导出
module.exports = merge(commonConfig,devConfig);
// package.json

"scripts":{
 	"dev":"webpack-dev-server --config webpack.dev.js",
 	"build":"webpack --config webpack.prod.js"
}

四、总结

1. 可用于生产环境

  • 优化 loader 处理范围
  • 优化 resolve 配置
  • IgnorePlugin
  • happyPack
  • tree Shaking
  • ParallelUglifyPlugin
  • 代码分割 code Splitting
  • 作⽤域提升(Scope Hoisting)
  • 使用 CDN 优化

2. 不可用于生产环境

  • Hot Module Replacement (HMR:热模块替换)
  • 使⽤⼯具量化
  • DllPlugin、HardSourceWebpackPlugin