性能优化(一)

2,611 阅读11分钟

代码分离

webpack在默认构建的时候,会将所有的代码打包到一个文件中,例如bundle.js

bundle.js中包含往往包含如下内容:

  1. 自己编写的代码
  2. 第三方库代码
  3. webpack为了支持模块化 而添加的额外的运行时代码

而随着业务的发展,bundle.js会越发庞大

  1. 所有的代码集中在一个文件中,不利于我们对构建后的代码进行管理和维护

  2. 所有的代码集中到一个文件中,无法做到按需加载或并行加载

    用户需要一次性下载全部的代码文件,这会大大降低首屏的渲染速度

所以代码分离(Code Splitting)是webpack一个非常重要的特性

  • 它主要的目的是将代码分离到不同的bundle中,之后我们可以按需加载,或者并行加载这些文件

  • 比如默认情况下,所有的JavaScript代码(业务代码、第三方依赖、暂时没有用到的模块)在首页全部都加载,就会影响首页的加载速度

  • 代码分离可以分出更小的bundle,以及控制资源加载优先级,提高代码的加载性能

    即那些文件需要被下载,那些文件需要在浏览器空闲的时候下载,那些文件是到要使用了才进行下载

常见代码分离方式

  1. 入口起点
  2. 防止重复: 使用Entry Dependencies或者SplitChunksPlugin去重和分离代码 --- 使用SplitChunksPlugin进行自定义分离配置
  3. 动态导入: 通过模块的内联函数调用来分离代码

入口起点

入口起点 就是 使用entry配置手动分离代码, 配置多个入口, 根据多个入口生成多个依赖关系图, 最终打包出多个bundle

  entry: {
    // name: 路径
    main: './src/main.js',
    index: './src/index.js'
  },
  output: {
    path: path.resolve(__dirname, 'build'),
    // 多入口需要输出多个bundle
    // 所以使用placeholder来进行占位 -- 其中name就是entry中配置的key(如name,index)
    filename: '[name]-bundle.js',
    clean: true
  }

但是此时如果我们在多个入口中依赖了重复的模块,那么这些重复的模块会被宰多个入口中重复被打包

所以为了解决这个问题,我们可以单独配置那些模块需要进行共享

  entry: {
    main: {
      // 入口文件
      import: './src/main.js',
      // 依赖的需要共享的第三方库
      dependOn: 'shared1'
    },
    index: {
      import: './src/index.js',
      dependOn: 'shared1'
    },
    // 自定义所需要依赖的第三方库
    shared1: 'axios',
    shared2: ['dayjs', 'loadsh']
  },
  output: {
    path: path.resolve(__dirname, 'build'),
    filename: '[name]-bundle.js',
    clean: true
  }

动态导入 (dynamic import)

当我们使用import函数来引入一个模块的时候,

表示我们并不确定这个模块中的代码一定会用到,所以最好拆分成一个独立的js文件

此时浏览器并不会直接下载该模块,而是等到我们真正使用到该模块的时候才去下载该模块

console.log('main')

const btn = document.createElement('button')
btn.textContent = 'index button'
document.body.appendChild(btn)

btn.onclick = () => {
  // index.js 并不会立即下载
  // 而是等到点击按钮后才会被下载下来
  import('./index')
}

我们也可以单独设置chunk的名称

  output: {
    path: path.resolve(__dirname, 'build'),
    // 默认的name
    // 如果是bundle - name就是文件名
    // 如果是chunk(分的子包) - name是路径_文件名_后缀
    filename: '[name]-bundle.js',
    // filename 是设置bundle的命名规则
    // chunkFilename是设置chunk的命名规则
    // 如果没有设置chunkFilename,那么chunk的命名规则采用filename的命名规则
    // ps: Filename - 是一个单词
    chunkFilename: '[name]-chunk.js',
    clean: true
  }
// webpackChunkName是魔法注释 -- 也就是给webpack看的注释
// 我们可以使用webpackChunkName来指定对应chunk的名称
// ps: webpackChunkName - chunk和name是两个单词
import(/* webpackChunkName: 'index' */'./index')s

splitChunk

我们可以通过splitChunk来实现自定义webpack分包配置

splitChunk底层是使用SplitChunksPlugin来实现

该插件webpack已经默认安装和集成,所以我们并不需要单独安装和直接使用该插件

只需要提供SplitChunksPlugin相关的配置信息即可

// 优化信息
optimization: {
  // 自定义分包信息
  splitChunks: {
    // 设置那些模块需要分包
    // 默认值 async 也就是异步包(即那些import函数)引入的包需要单独分包
    // inital 只对同步模块进行打包
    // all 表示import方法引入的包和node_modules下被使用的包都需要进行分包
    // 第三方包会被打包到vednors-node_modules-xxx-xxxx.js中
    chunks: 'all'
  }
}
optimization: {
  splitChunks: {
    chunks: 'all',
      // 当包大于20000b的时候,对包进行拆分
      // ps: 分出的包可能大于20000b
      // 因为webpack在进行拆包的时候,会尽可能的根据依赖所使用的第三方包进行拆分
      // 但有的时候拆分完毕后,依旧大于20000b,但是代码已经无法继续进行拆分
      // 如一个类或方法是无法进行拆分的,此时拆分出的包就可能大于20000b
      maxSize: 20000,
        // 拆分后的包不能低于10000b -- 默认值 20000
        minSize: 10000
  }
}
optimization: {
    splitChunks: {
      chunks: 'all',
      minSize: 10,
      // 自定义分包规则
      cacheGroups: {
        // [name]: 配置对象
        vendors: {
          // 匹配的正则表达式
          // 此处正则匹配的是/node_modules/
          // 因为windows下的路径分隔符为\
          // 而mac下的是/
          // 所以使用[\\/]作为路径分隔符
          test: /[\\/]node_modules[\\/]/,
          filename: '[name].js'
        },
        math: {
          test: /[\\/]math[\\/]/,
          filename: '[name].js'
        }
      }
    }
  }

注释提取

默认情况下,webpack在进行分包时,会对包中和版权相关的注释进行单独提取

image.png

const TerserPlugin = require('terser-webpack-plugin')

optimization: {
  // 可能进行多个优化,如js的优化,css的优化等
  // 所以对应的值是数组
  minimizer: [
    // 抽取版权信息是通过terser-webpack-plugin来实现的
    // 而terser-webpack-plugin基于的是terser
    new TerserPlugin({
      // 不抽取对应的代码
      extractComments: false
    })
  ]
}

optimization

chunkIds

optimization.chunkIds配置用于告知webpack模块的id采用什么算法生成

也就是用来配置placeholder中id的值是怎么生成的

说明
natural按照文件index进行递增形成的数字
文件发生改变的时候,对应的文件名可能发生改变,因为index发生了改变
natural基本不使用
1. 即使文件内容不发生改变,index发生了改变,对应的整个文件也需要重新进行打包
2. 如果名称不变的时候,浏览器会优先使用缓存,而不是请求新文件
但是natural的时候,文件名经常容易发生改变,不利于浏览器的缓存,需要重新下载整个文件
named一个可读的名称的id,对应的值和[name]是一致的
development环境下的默认值
deterministic在不同的编译中不变的短数字id,也就是说一个文件对应一个数字id
production环境下的默认值
optimization: {
  chunkIds: 'deterministic'
}

runtimeThunk

webpack是模块打包器,所以在构建后会存在一定的runtime代码

runtime相关的代码指的是在运行环境中,负责加载、解析和执行模块,并维护模块之间的依赖关系

我们可以选择在构建后 ,将runtime相关的代码单独抽离,这样可以将业务代码和runtime相关的代码进行分离

这样可以保证如果我们修改了业务代码后,runtime相关的代码是不需要被重新构建,有利于提升构建速度和浏览器缓存

optimization: {
  // runtimeChunk可选值
  // 1. true/multiple --- 多入口环境下,针对于每一个入口单独打包对应的runtime
  // 2. single --- 仅仅只打包一个runtime文件
  // 3. 一个有name属性的对象 --- name会作为构建时placeholder中name的值
  // runtime构建后的代码是bundlle, output.filename -> [name]-bundle.js
  // runtime构建后 则为 runtime-bundle.js
  runtimeChunk: {
    name: 'runtime'
  }

注意: runtime是bundle代码,不是chunk代码

在webpack中,bundle是是指哪些可以直接在浏览器上运行的模块

而chunk是指哪些在bundle代码运行过程中,通过runtime中的模块化代码加载的文件

也就是说chunk文件不能直接在浏览器中执行,而是需要通过bundle进行引入,是bundle文件的一部分

preload 和 prefetch

浏览器在加载页面存在三个步骤

  1. 下载对应资源
  2. 加载并解析对应资源
  3. 渲染解析结果

使用preload和prefetch可以预先下载对应资源,从而减少页面加载和解析所需要的时间

preload和prefetch往往结合import方法一起使用

方式名称说明
preload预加载和主包一起并行下载
preload的资源在主包解析过程中是必不可少的
例如当前页面相关的JS文件和CSS文件
prefetch预获取等到所有资源下载完成后 (也就是浏览器空闲时)再去下载对应的资源
prefetch是等空闲后再去加载的资源,所以一般是下一个页面的相关数据和资源
import(
  /* webpackChunkName: 'math' */
  /* webpackPrefetch: true */
  './math/math')

CDN

CDN称之为内容分发网络(Content Delivery Network或Content Distribution Network,缩写:CDN)

  • 它是指通过相互连接的网络系统,利用最靠近每个用户的服务器
  • 更快、更可靠地将音乐、图片、视频、应用程序及其他文件发送给用户
  • 来提供高性能、可扩展性及低成本的网络内容传递给用户

CDN服务器一般需要购买,如阿里、腾讯、亚马逊、Google等

但也有一些开源的CDN服务,如unpkg、JSDelivr、cdnjs,bootCDN(国内)等

image.png

在开发中,我们使用CDN主要是两种方式

  1. 打包的所有静态资源,放到自己购买的CDN服务器上,用户所有资源都是通过CDN服务器加载的

  2. 项目文件依旧存放在自己服务器上,但是一些第三方资源放到CDN服务器上

打包所有静态资源到CDN服务器

<!-- 
	默认情况下,打包后对应的资源是存放在本地的
	假设服务器地址为 http://www.example.com
	那么请求的路径即为 http://www.example.com/main-bundle.js
-->
<script defer src="runtime-bundle.js"></script>
<script defer src="main-bundle.js"></script>

配置

  output: {
    path: path.resolve(__dirname, 'build'),
    filename: '[name]-bundle.js',
    chunkFilename: '[name]-chunk.js',
    // 配置所有资源的基准路径
    // 默认是 output.path
    publicPath: 'https://cdn.example.com',
    clean: true
  }

此时构建后的文件就变成了

<script defer src="https://cdn.example.com/runtime-bundle.js"></script>
<script defer src="https://cdn.example.com/main-bundle.js"></script>

第三方资源存放到CDN服务器

// 假设项目中使用了react和dayjs
import react from 'react'
import dayjs from 'dayjs'
// webpack.config.js
// externals表示需要再构建的时候排除那些第三方库
// 这是一个和output,resolve,module同级的属性
externals: {
  // import react from 'react' 这个库是从react这个包引入的
  // 所以这里的key的值是dayjs
  // react对应的CDN地址为 https://unpkg.com/react@18.2.0/umd/react.production.min.js
  // 在这个CDN地址中所挂载到全局的那个对象是React
  // 所以这里的value值是React
  dayjs: 'dayjs',
    react: 'React'
}
<!-- html的模板文件 -->
<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8" />
		<meta http-equiv="X-UA-Compatible" content="IE=edge" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
		<title>Document</title></script>
	</head>
	<body>
		<div id="root"></div>
    <!-- 
			因为对应第三方已经在构建的时候被移除
			所以我们在构建的时候,需要在html模板中手动引入对应的CDN地址
		-->
    <script src="https://unpkg.com/react@18.2.0/umd/react.production.min.js"></script>
    <script src="https://unpkg.com/dayjs@1.11.7/dayjs.min.js"></script>
	</body>
</html>

MiniCssExtractPlugin

MiniCssExtractPlugin可以帮助我们将css提取到一个独立的css文件中

npm install mini-css-extract-plugin -D
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = {
   module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          // 替代style-loader
          // style-loader是将css使用style标签的方式进行插入
          // MiniCssExtractPlugin.loader是使用link标签的方式进行插入
          MiniCssExtractPlugin.loader,
          
         	// 将css解析为js文件,方便webpack进行构建
          'css-loader'
        ]
      }
    ]
  },
	plugins: [
    // 将css文件单独抽离成一个独立的模块
    new MiniCssExtractPlugin({
      // filename和chunkFilename都可以使用指定文件路径(如css/或js/)的方式来将对应文件打包到某一个文件夹下
      // import指令导入的css文件单独抽取到css文件夹下的[name].css
      filename: 'css/[name].css',
      // import方法导入的css文件单独抽取到css文件相爱的[name]_chunk.css
      chunkFilename: 'css/[name]_chunk.css'
    })
  ]
}

placeholder

placeholder是指一种占位符语法,可以在代码中插入动态值

在构建过程中,Webpack会根据配置的规则,将这些占位符替换成对应的值

placeholder中的hash都是通过MD4的散列函数处理后,生成一个128位的hash值(32个十六进制)

可以通过[hash:8]来控制hash值 的长度

placeholder说明
name文件名 -- filname
ext文件后缀名 --- 带点前缀
id为文件生成唯一标识
hash基于整个项目生产的hash值
任何一个文件对应的hash值都是一致的
只要项目中有一个文件发送了改变
那么整个项目对应的hash值都会发生改变
不推荐 --- 不利于缓存
chunkhash基于分包的chunk内容计算得出的hash值
一个文件foo.js,引入了foo.css
如果使用chunkhash, 那么修改了foo.css或者foo,js中任意一个文件,另一个文件不进行任何修改
foo.css和foo.js对应的hash值都会改变,且他们对应的hash值是一致的
contenthash基于文件内容得出的hash值
只有文件本身发生了修改,对应的contenthash才会发生修改
如果引入的文件发送了修改,是不会修改文件本身的contenthash
所以contenthash是被推荐使用的hash模式