【webpack】从webpack谈性能优化(一)
一、性能指标
进行性能优化之前首先要知道要在哪些方面做性能优化。
性能指标是衡量网页或应用程序性能的关键指标。那么我们常见的性能指标有哪些呢?
和FCP
- 首次绘制(First Paint ,FP)是指浏览器在加载页面时,首次将任何像素绘制到屏幕上的时间点。
- 它表示页面开始呈现的时刻,即使内容并不完整或不可见。
- 首次绘制并不关注绘制的具体内容,只关注是否有像素被绘制在屏幕上。
- 首次渲染时间(First Contentful Paint,FCP)是指浏览器在加载页面时,首次将页面的有意义内容(文本、图像、背景色等)绘制到屏幕上的时间点。
- FCP标志着用户首次能够看到页面的有意义内容,这对于提供更好的用户体验至关重要。
- 区别
- 首次绘制只关注页面开始呈现的时刻,而首次内容绘制则关注页面的有意义内容。
- 首次绘制时间通常比首次内容绘制时间更早,因为浏览器可以立即开始绘制页面的空白区域或背景色,但有意义内容的加载需要更多时间。
FP 指的是绘制像素,比如说页面的背景色是灰色的,那么在显示灰色背景时就记录下了 FP 指标。但是此时 DOM 内容还没开始绘制,可能需要文件下载、解析等过程,只有当 DOM 内容发生变化才会触发,比如说渲染出了一段文字,此时就会记录下 FCP 指标。因此说我们可以把这两个指标认为是和白屏时间相关的指标,所以肯定是最快越好。
最大内容绘制 (Largest Contentful Paint,LCP)是指视窗中最大的可见内容元素绘制完成的时间点。
- 具体来说,LCP 衡量了从开始加载页面到最大的可见内容元素(如图片、文本块、视频等)完全渲染到页面上的时间。
- 可交互时间(Time to Interactive,TTI)指页面加载完成后,用户能够与页面进行有效交互的时间点。
- 也可以理解为页面加载完成并且能够响应用户交互的时间点。
- TTI 测量首次内容绘制 (FCP)之后页面可靠地为用户交互做好准备的 最早时间。简单来说,快速的 TTI 有助于确保页面可用。
页面在以下情况下认为页面是完全交互的:
- 页面显示有用的内容;
- 为大多数可见页面元素注册了事件处理程序;
- 页面在50毫秒内响应用户交互
总之,TTI 关注的是整个页面的加载和渲染,并且它代表了页面可以进行有效交互的时间点,是衡量用户体验的重要指标,表示用户可以开始与页面进行有意义的交互操作的时间。
首次输入延迟(First Input Delay,FID)指用户首次与页面进行交互(如点击按钮、输入框等)的延迟时间。FID衡量用户对页面的第一次交互操作的响应时间,即用户点击或触摸页面元素后,到页面开始响应并进行相应处理的时间。
- 延迟是因为浏览器的主线正忙于做其他事情,所以不能响应用户,发生这种情况的一个常见原因是浏览器正忙于解析和执行应用程序加载的大量计算的JavaScript。
总阻塞时间(Total Block Time,TBT)用于衡量页面加载过程中发生的主线程阻塞时间总和。它被认为是页面加载过程中用户感知的延迟和交互性问题的一个重要指标。
- 它记录了在 FCP 到 TTI 之间所有长任务(超过50ms的任务)的阻塞时间总和。
累计位移偏移(Cumulative Layout Shift,CLS)会测量在页面整个生命周期中发生的每个意外的布局移位的所有单独布局移位分数的总和,他是一种保证页面的视觉稳定性从而提升用户体验的指标方案。
- CLS 旨在衡量整个页面生命周期中布局变化所导致的视觉不稳定性。
- 这包括页面加载时、资源加载完成后和用户与页面交互时的布局变化。
二、使用webpack进行性能优化
进行性能优化的手段有很多,比如就代码层面而言,使用路由懒加载、图片资源懒加载、第三方库的按需引入等,或者使用gzip压缩,使用浏览器缓存、CDN 缓存和服务端缓存等缓存,减少网络请求,提高访问速度。本篇文章从 webpack 层面详谈如何使用 webpack 进行性能优化。
使用webpack进行性能优化,我们可以将其分为:
- 打包结果的优化,
- 分包处理
- 减小包体积等
- 打包速度的优化
代码分离
-
它的主要目的是将代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件
-
比如默认情况下,所有的JS代码(业务代码、第三方依赖等)在首页全部都加载的话,就会影响首页的加载速度
-
代码分离可以获取更小的 bundle、控制资源加载优先级
常用的代码分离方法有三种:
- 入口起点:使用 entry 配置手动地分离代码。
- 防止重复:使用 入口依赖 或者 SplitChunksPlugin 去重和分离 chunk。
- 动态导入:通过模块的内联函数调用分离代码。
1.1 入口起点
entry: {
index: './src/',
main: './src/',
},
output: {
filename: '[name].',
path: path.resolve(__dirname, 'dist'),
},
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
存在的问题:
- 如果入口 chunk 之间包含一些重复的模块,那么这些重复模块都会被引入到各个 bundle 中。
- 这种方法不够灵活,并且不能动态地拆分应用程序逻辑中的核心代码。
1.2 防止重复
入口依赖
假设我们的 和 都引入过lodash
,上述的方法就会导致了重复引入,此时就可以在配置文件中配置 dependOn
选项,以在多个 chunk 之间共享模块
entry: {
index: { import: './src/', dependOn: 'shared' },
main: { import: './src/', dependOn: 'shared' },
shared: 'lodash',
},
output: {
filename: '[name].',
path: path.resolve(__dirname, 'dist'),
},
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
如果想要在一个 HTML 页面上使用多个入口,还需设置 : 'single'
SplitChunks
它底层是使用SplitChunksPlugin来实现的:
- 因为该插件webpack已经默认安装和集成,所以我们并不需要单独安装和直接使用该插件
- 只需要提供SplitChunksPlugin相关的配置信息即可
可以将公共的依赖模块提取到已有的入口 chunk 中,或者提取到一个新生成的 chunk。
optimization: {
splitChunks: {
chunks: 'all',
},
},
- 1
- 2
- 3
- 4
- 5
SplitChunks自定义配置解析
- chunks
- 有效值为
all
,async
和initial
- 默认值为 async
- all 表示对同步和异步代码都进行处理
- 有效值为
- minSize
- 生成 chunk 的最小体积(以 bytes 为单位)
- 如果一个包拆分出来达不到 minSize,那么这个包就不会拆分
- maxSize
- 将大于maxSize的包,拆分为不小于minSize的包
- cacheGroups
- 用于对拆分的包进行分组,作用就相当于是一个分组条件,满足这个条件输出为一个chunks
- test:可以是一个函数也可以是一个正则表达式,匹配符合规则的包
- name:拆分 chunk 的name属性
- filename:拆分包的名称,可以使用所有占位符
1.3 动态导入
webpack 提供了两个类似的技术实现动态拆分代码
- 推荐选择的方式,是使用符合 ECMAScript 的
import
实现动态导入。 - 使用 webpack 的遗留功能,使用 webpack 特定的 。
动态引入使用最多的一个场景是懒加载(比如路由懒加载):
比如我们有一个模块 :
- 该模块我们希望在代码运行过程中来加载它(比如判断一个条件成立时加载);
- 因为我们并不确定这个模块中的代码一定会用到,所以最好拆分成一个独立的js文件;
- 这样可以保证不用到该内容时,浏览器不需要加载和处理该文件的js代码
- 这个时候我们就可以使用动态导入
动态导入的文件命名:
-
因为动态导入通常是一定会打包成独立的文件的,所以并不会在cacheGroups中进行配置;
-
那么它的命名我们通常会在output中,通过 chunkFilename 属性来命名
output: { filename: '[name].', chunkFilename: 'chunk_[name].js', path: path.resolve(__dirname, 'dist'), },
- 1
- 2
- 3
- 4
- 5
- 6
-
你会发现默认情况下我们获取到的 [name] 是和id的名称保持一致的
- 如果我们希望修改name的值,可以通过magic comments (魔法注释)的方式
- 它通过在 import 中添加注释,我们可以进行诸如给 chunk 命名或选择不同模式的操作。
import( /* webpackChunkName: "my-chunk-name" */ './module');
- 1
filename 和 chunkFilename 的区别可以参考这篇文章:webpack 中 的filename 和 chunkFileName
1.4 预获取/预加载模块
webpack v4.6.0+ 增加了对预获取和预加载的支持
在声明 import
时,使用下面这些内置指令,来告知浏览器
- prefetch(预获取):将来某些导航下可能需要的资源
- preload(预加载):当前导航下可能需要资源
import(/* webpackPrefetch: true */ './path/to/');
- 1
与prefetch 指令相比,preload 指今有许多不同之处:
- preload chunk 会在父chunk 加载时,以并行方式开始加载。prefetch chunk 会在父chunk 加载结束后开始加载。
- preloadchunk 具有中等优先级,并立即下载。prefetch chunk在浏览器闲置时下载
- preloadchunk会在父chunk 中立即请求,用于当下时刻。prefetch chunk会用于未来的某个时刻
总之,可以通过代码拆分和按需加载进行性能优化。
使用 webpack 的代码拆分功能,将应用程序拆分成多个块(chunk),并按需加载。这样可以减小打包后的文件体积,提高初始加载速度。可以通过配置 webpack 的代码分割和懒加载策略,将不常用的模块延迟加载。