企业级项目:webpack中的性能优化

webpack性能优化概述

很多很多人都认为性能是一个项目必不可少的,我总结了有关webpack构建项目中的性能优化的几个方面。在了解性能优化之前,最好对webpack编译原理有所了解,方便更深入的学习。
可以参考:大神眼中的webpack构建工具:对编译原理的分析

本文中性能优化目录:

  • 构建性能:是指在开发阶段的构建性能,而不是生产环境的构建性能,尽可能提高开发效率
    • 减少模块解析:
    • 优化loader性能
    • 热替换
  • 传输性能:服务端的JS传输给客户端的时间。总代码量越少,时间越少。文件数量越少,http请求次数越少。
    • 分包
      • 手动分包
      • 自动分包
    • 体积优化
      • 代码压缩
      • tree shaking
    • 懒加载
    • gzip
  • 运行性能:在浏览器端的运行速度。
    • 运行性能主要在书写代码中体现

一、构建性能

1、减少模块解析

在这里插入图片描述
模块解析包括:抽象语法树分析、依赖分析、模块语法替换。如果一个模块不做模块解析,那么经过loaders处理后的代码就是最后的源码。但是模块解析又是必须要做的步骤,那么如何减少模块解析?嘿嘿。如果一个模块中没有其他依赖就可以不对其进行模块解析,其实,减少模块解析主要是针对一些已经打包好的第三方库,比如jquery等。配置一个模块不进行解析很简单,只要在module中配置noParse。一个正则表达式。

module.exports = {
    mode: "development",
    devtool: "source-map",
    module: {
        rules:[],
        noParse: /jquery/
    }
}

2、优化loader性能

(1)减少loader应用范围

优化loader的性能,其实就是进一步限制loader的应用范围,对于某些库,不需要使用loader,比如说babel-loaderbabel-loader是将某些ES2015+转换为浏览器识别的语法。但是某些库,本来就没有使用这么高版本的语法,使用loader处理完全是浪费时间,所以不需要对其进行loader处理了呀。比如loadsh库。我们可以通过配置,让其跳过loader处理。

通过module.rule.excludemodule.rule.include,排除或仅包含需要应用loader的场景

module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /lodash/,
                use: "babel-loader"
            }
        ]
    }
}

当然,第三方大部分库都已经对其进行了babel处理,如果暴力一点,甚至可以排除掉node_modules目录中的模块,或仅转换src目录的模块。但是要慎重,排除之前要去官网看看是否已经处理过了。

module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                //也可以用 include: /src/,效果相同
                use: "babel-loader"
            }
        ]
    }
}

(2)缓存loader的结果

如果某个文件内容不变,经过相同的loader解析后,解析后的结果也不变,所以我们可以将loader的解析结果保存下来,让后续的解析直接使用缓存的结果,当然这种方式会增加第一次构建时间

cache-loader可以实现这样的功能,要将cache-loader放在最前面,虽然放在最前面,但是他可以决定让后续loader是否运行。具体配置看官方文档!

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
            {
                loader: "cache-loader",
                options:{
                  cacheDirectory: "./cache" //缓存的目录
                }
          	}, ...loaders] //其余的loaders
      },
    ],
  },
};

实际上,loader的运行过程中,还包含一个过程,即pitch

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l6TffEJ7-1584435464160)(F:\博客\前端工程化\assets\pitch-loader运行过程.png)]

(3)为loader开启多线程

如果loader进行处理的过程是一个异步操作的话,可以大大减少处理时间,thread-loader会开启一个线程池,它会把后续的loader放到线程池的线程中运行,以提高构建效率。因为后续的loader是放入新的线程池中,就无法使用webpack api、自定义的plugin api,也无法访问的webpack options。具体把thread-loader放在什么位置,要根据项目视情况而定,可以傻瓜式测试。但要注意的是,开启和管理新的线程也是需要时间的。

module.exports = {
  mode: "development",
  devtool: "source-map",
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          "file-loader",
          "thread-loader", //将thread-loader和babel-loader放入新的线程中
          "babel-loader"
        ]
      }
    ]
  }
};

3、热替换 HMR

我们在使webpack-dev-server开发服务器时,他会事时的监控代码变动,不需要重新带包,但是webpack-dev-server发现代码变动的时候,浏览器会刷新,重新请求所有资源。这显然不是我们开发最理想的结果,我们更希望,当我们更改一部分代码的时候,浏览器不刷新,只是局部进行替换。热替换就是实习了局部替换。要注意热替换不会讲题构建的性能,但是它可以降低代码变动到效果呈现的时间。

使用webpack-dev-server的流程:
在这里插入图片描述

使用热替换的流程:
在这里插入图片描述

使用热替换

  1. 更改配置:
module.exports = {
  devServer:{
    hot:true // 开启热替换
  },
  plugins:[ 
    new webpack.HotModuleReplacementPlugin() //使用插件
  ]
}
  1. 更改代码:随便一个文件写入以下代码,只要保证运行即可
// index.js
if(module.hot){ // 是否开启了热更新
  module.hot.accept() // 接受热更新
}

热替换原理

当在配置文件中开启了热替换后,webpack-dev-server会向打包结果中注入module.hot属性,所以在上述文件中添加的module.hot代码。默认情况下,webpack-dev-server不管是否开启了热更新,当重新打包后,都会调用location.reload刷新页面,但是如果运行了module.hot.accept(),就不会再调用loaction.reload()来刷新页面,而是使用websocket的方式,module.hot.accept()会让服务器更新的内容通过websocket传送给浏览器,仅仅是传输修改的部分。然后将结果交给插件HotModuleReplacementPlugin注入的代码执行,插件HotModuleReplacementPlugin会根据覆盖原始代码,然后让代码重新执行。

比如我在这里修改了一个js文件的模块导出,当监控到代码发生变化以后,websocket向客户端发出了两个服务,第一个是热替换的哈希值,不解释。

在这里插入图片描述

第二个就是需要热替换的代码,当接收到这个这段代码的时候,HotModuleReplacementPlugin插件就会根据key值"./src/a.js"来找到模块的位置,将模块的value重新覆盖。

在这里插入图片描述
module.hot.accept()的作用是让webpack-dev-server通过socket管道,把服务器更新的内容发送到浏览器
简单来说,热替换就是开启热替换的webpack-dev-server开发服务器监控到代码变化,通过websocket从服务的向客户端发送变化的内容,客户端接受到变化的内容后替换掉原内容。

样式热替换

对于样式也是可以使用热替换的,需要使用style-loader,因为热替换发生时HotModuleReplacementPlugin只会简单的重新运行模块代码。因此style-loader的代码一运行,就会重新设置style元素中的样式。而mini-css-extract-plugin,由于它生成文件是在构建期间,运行期间并会也无法改动文件,因此它对于热替换是无效的。

二、传输性能

1、手动分包

手动分包的总体思路是先单独打包公共模块,公共模块会被打包成一个动态链接库(ddl),并且形成一个资源清单。然后再根据入口模块进行正常的打包过程。

在这里插入图片描述
当正常打包时,如果发现模块中使用了资源清单中描述的模块,如下

//源码,入口文件index.js
import $ from "jquery"
import _ from "lodash"
_.isArray($(".red"));

由于资源清单中包含jquerylodash两个模块,因此打包结果不会出现jquerylodash的源代码,而是通过导出一个模块的方式,如下

(function(modules){
  //...
})({
  // index.js文件的打包结果并没有变化
  "./src/index.js":
  function(module, exports, __webpack_require__){
    var $ = __webpack_require__("./node_modules/jquery/index.js")
    var _ = __webpack_require__("./node_modules/lodash/index.js")
    _.isArray($(".red"));
  },
  // 由于资源清单中存在,jquery的代码并不会出现在这里
  "./node_modules/jquery/index.js":
  function(module, exports, __webpack_require__){
    module.exports = jquery;
  },
  // 由于资源清单中存在,lodash的代码并不会出现在这里
  "./node_modules/lodash/index.js":
  function(module, exports, __webpack_require__){
    module.exports = lodash;
  }
})

这样一来,重复代码就会减少,也就减少了传输时的体积。

(1)打包公共模块

打包公共模块是一个独立的打包过程,所以我们通常会重建一个配置文件webpack.dll.config.js,需要两个过程,首先打包公共模块,暴露变量名,然后用DllPlugin插件生成资源清单

const path = require("path")
const webpack = require("webpack")
module.exports = {
  mode: "production",
  entry: {//打包公共模块
    jquery: ["jquery"],
    lodash: ["lodash"]
  },
  output: {
    filename: "dll/[name].js",
    library: "[name]"//
  },
  plugins: [//生成资源清单
    new webpack.DllPlugin({
      path: path.resolve(__dirname, "dll", "[name].manifest.json"), //资源清单的保存位置
      name: "[name]"//资源清单中,暴露的变量名
    })
  ]
};

运行后,即可完成公共模块打包

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

(2)使用公共模块

  1. 在页面中手动引入公共模块
<script src="./dll/jquery.js"></script>
<script src="./dll/lodash.js"></script>
  1. 为了避免把公共模块清除,需要重新设置clean-webpack-plugin,如果没有使用你该插件则忽略
new CleanWebpackPlugin({
  // 要清除的文件或目录
  // 排除掉dll目录本身和它里面的文件
  cleanOnceBeforeBuildPatterns: ["**/*", '!dll', '!dll/*']
})

目录和文件的匹配规则使用的是globbing patterns

  1. 使用DllReferencePlugin,告诉webpack资源清单的位置,如果遇到导出模块已经在资源清单中,则不需要再进行打包。
module.exports = {
  plugins:[
    new webpack.DllReferencePlugin({
      manifest: require("./dll/jquery.manifest.json")
    }),
    new webpack.DllReferencePlugin({
      manifest: require("./dll/lodash.manifest.json")
    })
  ]
}

简单来说,手动打包首先要开启output.library暴露公共模块,使用webpack.DllPlugin插件生成资源清单(可以不使用,自己写),然后在页面中引入资源清单中的依赖,最后用DllReferencePlugin插件使用资源清单。

在手动打包的过程中,我们需要注意,资源清单是不参与运行的,所以不能把资源清单放在打包目录中。

手动打包优点

  1. 极大提升自身模块的打包速度
  2. 极大的缩小了自身文件体积
  3. 有利于浏览器缓存第三方库的公共代码

缺点

  1. 使用非常繁琐
  2. 如果第三方库中包含重复代码,则效果不太理想

2、自动分包

自动包区别于手动分包的是不需要确定具体为那个模块分包,而是从一个宏观的角度来控制分包,那么要控制分包,就需要有一个合理的分包策略。webpack4已经放弃了原来用CommonsChunkPlugin实现分包,而是在内部使用SplitChunksPlugin进行分包。

分包流程:分包时,webpack根据分包策略,实现具体的分包,它会开启一个新的chunk,对分离的模块进行打包处理。公共代码会生成新chunk即common,最后打包成budle_common.js 如图所示:
在这里插入图片描述

自动分包原理:自动分包会检查每个chunk编译的结果,根据分包策略,找到那些满足策略的模块,并生成新的chunk打包这些模块,再将打包出去的模块从原来的包中移除,并修改原来包的代码

(1)分包策略的基本配置

webpack提供了optimization配置项,用于配置一些优化信息,其中splitChunks是分包策略的配置,其中有以下常用配置

  • chunks:该配置项用于配置需要应用分包策略的chunk,有以下三个值,默认时async
    • all: 对于所有的chunk都要应用分包策略,一般来说使用这个值
    • async:仅针对异步chunk应用分包策略
    • initial:仅针对普通chunk应用分包策略
  • maxSize:如果一个要被分出来的包超过了该值,webpack就会尽可能的将其分成多个包。注意:分包的基础单位是模块,如果一个完整的模块超过了该体积,它是无法做到再切割的,因此,尽管使用了这个配置,完全有可能某个包还是会超过这个体积。通常不使用
  • automaticNameDelimiter:新chunk名称的分隔符,默认值~
  • minChunks:一个模块至少被多少个chunk使用时,才会进行分包,默认值1
  • minSize:当分包达到多少字节后才允许被真正的拆分,默认值30000
module.exports = {
  optimization: {
    splitChunks: {
      //分包配置
      chunks: "all",
      //maxSize: 60000
      automaticNameDelimiter: ".",
      minChunks: 2,
      minSize: 30000
    }
  }
}

(2)缓存组

实际上,分包策略是基于缓存组的。每个缓存组提供一套独有的策略,webpack按照缓存组的优先级依次处理每个缓存组,被缓存组处理过的分包不需要再次分包。默认情况下,webpack提供了两个缓存组,很多时候,缓存组对于我们来说没什么意义,因为默认的缓存组就已经够用了。

webpack默认缓存组

module.exports = {
  optimization:{
    splitChunks: {
      chunks:"all",
      //全局配置
      cacheGroups: {
        // 属性名是缓存组名称,会影响到分包的chunk名
        // 属性值是缓存组的配置,缓存组继承所有的全局配置,也有自己特殊的配置
        vendors: { 
          test: /[\\/]node_modules[\\/]/, // 当匹配到相应模块时,将这些模块进行单独打包
          priority: -10 // 缓存组优先级,优先级越高,该策略越先进行处理,默认值为0
        },
        default: {
          minChunks: 2,  // 覆盖全局配置,将最小chunk引用数改为2
          priority: -20, // 优先级
          reuseExistingChunk: true // 重用已经被分离出去的chunk
        }
      }
    }
  }
}

通过缓存组对公共样式分离:webpack.config.js

const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin")
module.exports = {
  optimization: {
    splitChunks: {
      chunks: "all",
      cacheGroups: {
        styles: {
          test: /\.css$/, // 匹配样式模块
          minSize: 30000, 
          minChunks: 2
        }
      }
    }
  },
  module: {
    rules: [{ test: /\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader"] }]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      template: "./public/index.html",
      chunks: ["index"]
    }),
    new MiniCssExtractPlugin({
      filename: "[name].[hash:5].css",
      // chunkFilename是配置来自于分割chunk的文件名
      chunkFilename: "common.[hash:5].css" 
    })
  ]
}

3、代码压缩

为生产环境进行代码压缩,减少代码体积是增加传输性能必不可少的环节,进行代码压缩同时也可以破坏代码可读性,提升破解成本,目前流行的代码压缩工具主要有UglifyJsTerser

UglifyJs是一个传统的代码压缩工具,已存在多年,曾经是前端应用的必备工具,但由于它不支持ES6语法,所以目前的流行度已有所下降。

Terser是一个新起的代码压缩工具,支持ES6+语法,因此被很多构建工具内置使用。

Terser官网:https://terser.org/

webpack已经内置了Terser,所以我们在启用生产环境后即可用其进行代码压缩。

webpack自动集成了Terser如果你想更改、添加压缩工具,又或者是想对Terser进行配置,使用下面的webpack配置即可

const TerserPlugin = require('terser-webpack-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = {
  optimization: {
    // 是否要启用压缩,默认情况下,生产环境会自动开启
    minimize: true, 
    minimizer: [ // 压缩时使用的插件,可以有多个
      new TerserPlugin(), 
      new OptimizeCSSAssetsPlugin()
    ],
  },
};

4、tree shaking

代码压缩可以移除模块内部的无效代码,而tree shaking可以移除模块之间的无效代码。比如说

// myMath.js
export function add(a, b){
  console.log("add")
  return a+b;
}

export function sub(a, b){
  console.log("sub")
  return a-b;
}

这个工具模块有两个导出方法,但是整个项目只使用了add,如果在打包的时候两个方法都打包的话无疑会增加无效代码量,tree shaking的作用就是移除无效的代码块。webpack2开始就支持了tree shaking。只要是生产环境,tree shaking自动开启

tree shaking工作原理webpack会从入口模块出发寻找依赖关系,当解析一个模块时,webpack会根据ES6的模块导入语句来判断,该模块依赖了另一个模块的哪个导出。依赖分析完毕后,webpack会根据每个模块每个导出是否被使用,标记未使用的导出为dead code,然后交给代码压缩工具处理。代码压缩工具最终移除掉那些dead code代码

在具体分析依赖时,webpack坚持的原则是:保证代码正常运行,然后再尽量tree shaking

我们在书写导入导出时,尽量使用以下方式:

  • 使用export xxx导出,而不使用export default {xxx}导出
  • 使用import {xxx} from "xxx"导入,而不使用import xxx from "xxx"导入

所以,如果你依赖的是一个导出的对象,由于JS语言的动态特性,以及webpack还不够智能,为了保证代码正常运行,它不会移除对象中的任何信息。

ES6的模块导入语句:使用ES6的模块导入语句,有利于更好的分析依赖,是因为ES6模块有以下特点:

  • 导入导出语句只能是顶层语句

  • import的模块名只能是字符串常量

  • import绑定的变量是不可变的

使用第三方库tree shaking注意

某些第三方库可能使用的是commonjs的方式导出,比如lodash又或者没有提供普通的ES6方式导出。对于这些库,tree shaking是无法发挥作用的。但好在很多流行的库都发布了它的ES6版本,比如lodash-es。我们在使用loadsh的时候可以使用lodash-es

副作用函数(side effect):函数运行过程中,可能会对外部环境造成影响的功能。如果函数中包含以下代码,该函数叫做副作用函数:异步代码localStorage对外部数据的修改

纯函数(pure function):如果一个函数没有副作用,同时,函数的返回结果仅依赖参数,则该函数叫做

webpack坚持的原则是:保证代码正常运行,然后再尽量tree shaking。因此当webpack无法确定某个模块是否有副作用时,它往往将其视为有副作用。当我们知道某个导出没有副作用,但是webpack担心common.js有副作用,如果去掉会影响某些功能,这时候我们就需要标记该文件是没有副作用的。当然,第三方插件中,一般都已经标记过了,我们无需自己添加。

package.json中加入sideEffects

{
    "sideEffects": false
    //"sideEffects": ["!src/common.js"]
}

5、懒加载

懒加载就是动态加载,按需加载,当我们需要的时候再加载。使用import()语法。import()会返回一个promise

if(Math.random()<0.5){
	const {add} =await import("./utils.js")
	const result = add(1,3)
}

此时,utils工具类是等到要执行时才会引入,而不会变成顶层语句直接执行。当我们执行到import时才会到服务端请求该模块的js文件,而不是在页面加载的时候就去请求,可以减少首页加载时间过长

6、gzip

gzip是一种压缩文件的算法,当我们js文件过大的时候,就可以使用gzip的方式,对文件进行压缩,配合服务器端进行使用。

具体使用参照1:
webpack-dev-server开发服务器 和 webpack中常用plugin和loader一文中的compression-webpack-plugin插件
具体使用参照2:
webpack+nginx实现gzip压缩解决vue首屏加载过慢

发布了33 篇原创文章 · 获赞 105 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/Newbie___/article/details/104925710