# 前端性能优化原理与实践
# webpack打包
在资源请求的过程中,涉及到网络请求的,包括:HTTP、TCP、DNS。其中TCP、DNS前端能做的工作非常有限,因此优化HTTP就成为了首要任务。
HTTP的优化可以从三个点出发:
- 减少请求次数;
- 减少每次请求的资源大小;
- 减少请求所花费的时间;
从前端的角度看,其实就是资源的压缩和合并,还有资源的缓存。这些事情webpack十分适合。
# webpack性能瓶颈
webpack打包有两个瓶颈:
- webpack 的构建过程太花时间
- webpack 打包的结果体积太大
# webpack优化方案
不要让loader做太多事情。比如说当使用babel-loader时,用 include 或 exclude 来帮我们避免不必要的转译。
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
除此之外,我们可以选择开启缓存将转译结果缓存至文件系统。只需要这么写:
loader: 'babel-loader?cacheDirectory=true'
# 优化第三方库
为了防止每次重复打包不会变动的第三方库,可以使用DllPlugin。
DllPlugin 是基于 Windows 动态链接库(dll)的思想被创作出来的。这个插件会把第三方库单独打包到一个文件中,这个文件就是一个单纯的依赖库。这个依赖库不会跟着你的业务代码一起被重新打包,只有当依赖自身发生版本变化时才会重新打包。
# Happypack
webpack是单线程的,Happypack会充分释放 CPU 在多核并发方面的优势,帮我们把任务分解给多个子进程去并发执行,大大提升打包效率。
下面是用法:
const HappyPack = require('happypack')
// 手动创建进程池
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length })
module.exports = {
module: {
rules: [
...
{
test: /\.js$/,
// 问号后面的查询参数指定了处理这类文件的HappyPack实例的名字
loader: 'happypack/loader?id=happyBabel',
...
},
],
},
plugins: [
...
new HappyPack({
// 这个HappyPack的“名字”就叫做happyBabel,和楼上的查询参数遥相呼应
id: 'happyBabel',
// 指定进程池
threadPool: happyThreadPool,
loaders: ['babel-loader?cacheDirectory']
})
],
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 压缩构建结果体积
首先我们需要知道打包的结果中,哪些资源是比较大的,可以使用webpack-bundle-analyzer (opens new window)来通过可视化的方式查看各个模块的大小以及依赖关系。
使用方式如下:
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}
2
3
4
5
6
7
其次,我们可以通过tree-shaking来删除多余的代码,该功能新版webpack
默认使用。
最后,我们可以通过Gzip
压缩来达到减少资源体积的目的。
# 按需加载
最经典的优化方式就是路由懒加载,只有当需要加载某个页面的时候,再去动态获取js文件。
vue-router
中路由懒加载的写法如下:
{
path: '/ruleResult',
component: () => import('@/views/rule/index'),
},
2
3
4
# 图片优化
时下应用较为广泛的 Web 图片格式有JPEG/JPG、PNG、WebP、Base64、SVG 等。
计算机中,像素用二进制数表示。一个二进制位表示两种颜色(0|1 对应黑|白),如果一种图片格式对应的二进制位数有 n
个,那么它就可以呈现 2^n
种颜色。
# JPEG/JPG
关键字:有损压缩、体积小、加载快、不支持透明
JPG 适用于呈现色彩丰富的图片,在我们日常开发中,JPG 图片经常作为大的背景图、轮播图或 Banner 图出现。
JPG呈现大图,保证质量,体积不大。
JPG的缺点是:当它处理矢量图形和 Logo 等线条感较强、颜色对比强烈的图像时,人为压缩导致的图片模糊会相当明显。JPEG 图像不支持透明度处理。
# PNG
关键字:无损压缩、质量高、体积大、支持透明
PNG(可移植网络图形格式)是一种无损压缩的高保真的图片格式。8 和 24,这里都是二进制数的位数。按照我们前置知识里提到的对应关系,8 位的 PNG 最多支持 256 种颜色,而 24 位的可以呈现约 1600 万种颜色。
缺点就是体积太大。主要用它来呈现小的 Logo、颜色简单且对比强烈的图片或背景等。
# SVG
关键字:文本文件、体积小、不失真、兼容性好
SVG(可缩放矢量图形)是一种基于 XML 语法的图像格式。它和本文提及的其它图片种类有着本质的不同:SVG 对图像的处理不是基于像素点,而是基于对图像的形状描述。
SVG的优点:文件体积更小,可压缩性更强。图片可以无限放大不失真。SVG是文本文件,比较灵活。 SVG的局限性:渲染成本比较高,存在学习成本。
用法:可以采用svg 标签进行编程,也可以通过.svg 文件进行引入。
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">
<circle cx="50" cy="50" r="50" />
</svg>
<img src="文件名.svg" alt="">
2
3
4
# Base64
关键字:文本文件、依赖编码、小图标解决方案
Base64 并非一种图片格式,而是一种编码方式。作为小图标或者小尺寸图片的解决方案。
base64的优点:Base64 是一种用于传输 8Bit 字节码的编码方式,通过对图片进行 Base64 编码,我们可以直接将编码结果写入 HTML 或者写入 CSS,从而减少 HTTP 请求的次数。
满足以下条件,可以使用base64:
- 图片的实际尺寸很小
- 图片无法以雪碧图的形式与其它小图结合(合成雪碧图仍是主要的减少 HTTP 请求的途径,Base64 是雪碧图的补充)
- 图片的更新频率非常低(不需我们重复编码和修改文件内容,维护成本较低)
# WebP
全能型选手,支持有损压缩和无损压缩。
WebP的优点:支持有损压缩和无损压缩。支持透明、可以显示动图、压缩后尺寸更小。
WebP的缺点:兼容性问题、增加服务器的负担。
# 浏览器缓存
浏览器缓存机制有四个方面,它们按照获取资源时请求的优先级依次排列如下:
- Memory Cache
- Service Worker Cache
- HTTP Cache
- Push Cache
# HTTP缓存
HTTP缓存分为强缓存和协商缓存。优先级较高的是强缓存,在命中强缓存失败的情况下,才会走协商缓存。
强缓存是利用 http 头中的 Expires 和 Cache-Control 两个字段来控制的。若命中强缓存,则不再请求服务器。命中强缓存的情况下,返回的 HTTP 状态码为 200。
# 强缓存
强缓存的头部包括expires和 cache-control。
expires 是一个时间戳,如果我们试图再次向服务器请求资源,浏览器就会先对比本地时间和 expires 的时间戳,如果本地时间小于 expires 设定的过期时间,那么就直接去缓存中取这个资源。若服务器与客户端存在时差,将带来意料之外的结果。
expires: Wed, 11 Sep 2019 16:12:18 GMT
Cache-Control
中的max-age
字段也允许我们通过设定相对的时间长度来达到同样的目的。max-age的单位是秒。相对时间可以规避掉时差问题。客户端会记录下请求到资源的时间点。
cache-control: max-age=31536000
Cache-Control
的 max-age
配置项相对于 expires
的优先级更高。当 Cache-Control
与 expires
同时出现时,我们以 Cache-Control
为准。
cache-control: max-age=3600, s-maxage=31536000
s-maxage
用于表示 cache
服务器上(比如 cache CDN)的缓存的有效时间的,并只对 public
缓存有效。s-maxage
仅在代理服务器中生效,客户端中我们只考虑max-age
。
# 协商缓存
协商缓存机制下,浏览器需要向服务器去询问缓存的相关信息,进而判断是重新发起请求、下载完整的响应,还是从本地获取缓存的资源。如果服务器判断资源没有改动,会返回304。
协商缓存的实现包括Last-Modified和Etag。
Last-Modified 是一个时间戳,如果我们启用了协商缓存,它会在首次请求时随着 Response Headers 返回:
Last-Modified: Fri, 27 Oct 2017 06:35:57 GMT
随后我们每次请求时,会带上一个叫 If-Modified-Since 的时间戳字段,它的值正是上一次 response 返回给它的 last-modified 值:
If-Modified-Since: Fri, 27 Oct 2017 06:35:57 GMT
服务器接受到该请求头时,会和资源最后修改时间进行对比。如果资源有变动,就会返回一个完整的响应内容,并在 Response Headers 中添加新的Last-Modified 值;否则,返回如上图的 304 响应,Response Headers 不会再添加Last-Modified 字段。
使用 Last-Modified
存在一些弊端:
- 编辑了文件,但是文件内容没有变化。但是服务器以为有变动。
- 修改文件速度过快,If-Modified-Since 只能检查到以秒为最小计量单位的时间差,因此服务器以为没有变动。
此时,需要使用Etag
进行补充。
Etag是由服务器为每个资源生成的唯一的标识字符串,这个标识字符串是基于文件内容编码的,只要文件内容不同,它们对应的 Etag 就是不同的。 当首次请求时,我们会在响应头里获取到一个最初的标识符字符串。
ETag: W/"2a3b-1602480f459"
当再次请求资源时,请求头里会携带一个值相同的、名为If-None-Match 的字符串:
If-None-Match: W/"2a3b-1602480f459"
Etag
的生成过程需要服务器额外付出开销,会影响服务端的性能,这是它的弊端。
Etag
在感知文件变化上比 Last-Modified
更加准确,优先级也更高。当 Etag
和 Last-Modified
同时存在时,以 Etag
为准。
# MemoryCache
MemoryCache,是指存在内存中的缓存。优先级高,效率也高。
当tab页关闭时,内存里的缓存也消失。哪些文件被放入缓存取决于浏览器,一般较小的文件会放入内存的缓存,大文件则不会。
# Service Worker Cache
借助 Service worker实现的离线缓存就称为Service Worker Cache。
# Push Cache
Push Cache 是指 HTTP2 在 server push 阶段存在的缓存。
- Push Cache 是缓存的最后一道防线。浏览器只有在 Memory Cache、HTTP Cache 和 Service Worker Cache 均未命中的情况下才会去询问 Push Cache。
- Push Cache 是一种存在于会话阶段的缓存,当 session 终止时,缓存也随之释放。
- 不同的页面只要共享了同一个 HTTP2 连接,那么它们就可以共享同一个 Push Cache。
# 本地存储
# Cookie
Cookie可以携带用户信息,当服务器检查 Cookie 的时候,便可以获取到客户端的状态。
Cookie以键值对的形式存在。Cookie最大存储4KB的内容。同一个域名下的所有请求,都会携带 Cookie。
# Web Storage
Web Storage分为 Local Storage 与Session Storage。
两者的区别在于生命周期和作用域的不同。
- 生命周期:
Local Storage
是持久化的本地存储,存储在其中的数据是永远不会过期的,使其消失的唯一办法是手动删除;而Session Storage
是临时性的本地存储,它是会话级别的存储,当会话结束(页面被关闭)时,存储内容也随之被释放。 - 作用域:
Local Storage
、Session Storage
和Cookie
都遵循同源策略。但Session Storage
特别的一点在于,即便是相同域名下的两个页面,只要它们不在同一个浏览器窗口中打开,那么它们的Session Storage
内容便无法共享。
Web Storage的特点是存储容量大,可以达到5-10M之间。不与服务端发生通信。
两者只能用于存储少量的简单数据。当遇到大规模的、结构复杂的数据时,就不适用了。
# IndexedDB
IndexedDB 是一个运行在浏览器上的非关系型数据库。理论上来说,IndexedDB 是没有存储上限的(一般来说不会小于 250M)。它不仅可以存储字符串,还可以存储二进制数据。
# 浏览器渲染
解析 HTML
在这一步浏览器执行了所有的加载解析逻辑,在解析 HTML 的过程中发出了页面渲染所需的各种外部资源请求。
计算样式
浏览器将识别并加载所有的 CSS 样式信息与 DOM 树合并,最终生成页面 render 树(:after :before 这样的伪元素会在这个环节被构建到 DOM 树中)。
计算图层布局
页面中所有元素的相对位置信息,大小等信息均在这一步得到计算。
绘制图层
在这一步中浏览器会根据我们的 DOM 代码结果,把每一个页面图层转换为像素,并对所有的媒体文件进行解码。
整合图层,得到页面
最后一步浏览器会合并合各个图层,将数据由 CPU 输出给 GPU 最终绘制在屏幕上。(复杂的视图层会给这个阶段的 GPU 计算带来一些压力,在实际应用中为了优化动画性能,我们有时会手动区分不同的图层)。
# css优化
浏览器解析css是从右往左匹配规则。我们要做到:
- 避免使用通配符。
- 关注可以通过继承实现的属性,避免重复匹配重复定义。
- 少用标签选择器。
- 减少嵌套。
# CSS和JS加载顺序优化
默认情况下,CSS 是阻塞的资源。浏览器在构建 CSSOM 的过程中,不会渲染任何已处理的内容。因此:需要将它尽早、尽快地下载到客户端,以便缩短首次渲染的时间。
解决方案:将CSS下载链接放到head标签内、使用CDN加载静态资源、合理使用preload和prefetch。
JS 引擎是独立于渲染引擎存在的。当 HTML 解析器遇到一个 script
标签时,它会暂停渲染过程,将控制权交给 JS 引擎。也就是说,JS 引擎抢走了渲染引擎的控制权。
可以通过对它使用 defer和 async 来避免不必要的阻塞。
JS有三种加载模式:
- 正常模式。默认情况,JS会阻塞浏览器。
async
模式。JS 不会阻塞浏览器做任何其它的事情。它的加载是异步的。脚本加载结束会立即执行。defer
模式。JS 不会阻塞浏览器。它的加载是异步的,执行被推迟。等整个文档解析完成、DOMContentLoaded
事件即将被触发时,被标记了defer
的 JS 文件才会开始依次执行。
# DOM优化
# 回流和重绘
回流:当对DOM的修改引发了DOM尺寸的变化时,浏览器需要重新计算元素的几何属性,然后将结果进行绘制。该过程为回流。
重绘:当对DOM的修改引发了样式的变化,但是没有尺寸变化时,浏览器不需要重新计算元素的几何属性,直接绘制新的样式。该过程为重绘。
结论:回流一定导致重绘,重绘不一定导致回流。
导致回流的操作有:
- 改变 DOM 元素的几何属性:修改诸如width、height、padding、margin、left、top、border等。
- 改变 DOM 树的结构:节点的增删和移动。
- 获取特定属性的值:诸如offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight以及getComputedStyle 方法。因为浏览器需要实时计算最新的值,会进行回流。
优化方案有:
- 缓存特定属性的值,防止频繁获取导致频繁回流。
- 避免逐条改变样式,使用类名去合并样式。
- 将DOM离线。也就是
display: none
。
# 减少DOM操作
使用document.fragment来减少DOM操作。
let container = document.getElementById('container')
// 创建一个DOM Fragment对象作为容器
let content = document.createDocumentFragment()
for(let count = 0; count < 10000; count++){
// span此时可以通过DOM API去创建
let oSpan = document.createElement("span")
oSpan.innerHTML = '我是一个小测试'
// 像操作真实DOM一样操作DOM Fragment对象
content.appendChild(oSpan)
}
// 内容处理好了,最后再触发真实DOM的更改
container.appendChild(content)
2
3
4
5
6
7
8
9
10
11
12
# Flush队列
当频繁回流或者重绘的时候,浏览器缓存一个flush队列,把我们触发的回流与重绘任务都塞进去,待到队列里的任务多起来、或者达到了一定的时间间隔,或者“不得已”的时候,再将这些任务一口气出队。
而获取特定属性的值就是不得已的时候,因此要避免频繁获取。
# 懒加载
懒加载:它是针对图片加载时机的优化。当页面上图片较多时,如果不做额外的处理,浏览器会将所有资源进行加载。如此会造成白屏、卡顿的不良影响。
懒加载的核心思路是:当元素出现在可视区域内,style 内联样式中的背景图片属性从 none 变成了一个在线图片的 URL。
在懒加载的实现中,有两个关键的数值:一个是当前可视区域的高度,另一个是元素距离可视区域顶部的高度。
当前可视区域的高度, 在和现代浏览器及 IE9 以上的浏览器中,可以用 window.innerHeight
属性获取。
而元素距离可视区域顶部的高度,选用 getBoundingClientRect()
方法来获取返回元素的大小及其相对于视口的位置。
// 获取所有图片标签
const imgs = document.getElementsByTagName('img');
const viewHeight = window.innerHeight;
// 元素距离顶部的距离比可是区域高度小,就意味着元素可以被用户看见,需要加载
const isView = (e) => (viewHeight - e.getBoundingClientRect().top) >= 0
// 记录检查到哪一张图片
let num = 0;
const lazyload = () => {
for (let i = num; i < imgs.length; i++) {
let currentEl = imgs[i];
if (isView(currentEl)) {
currentEl.src = currentEl.dataset.src;
num = i + 1;
}
}
}
// 监听Scroll事件
window.addEventListener('scroll', lazyload, false);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
需要注意的是,上面监听的滚动事件会频繁触发,因此需要进行防抖处理。
# 防抖与节流
原生事件中,有许多事件容易频繁触发。比如scroll 事件、resize事件、鼠标事件、键盘事件等等。 频繁触发回调导致的大量计算会引发页面的抖动甚至卡顿。因此需要防抖和节流来限制触发的频率。
# 节流
所谓的“节流”,是通过在一段时间内无视后来产生的回调请求来实现的。也就是说,一段时间内,以第一个请求为准。这段时间所有的其他请求都被忽略。
function throttle(fn, interval) {
// 通过闭包保存上一次触发回调的时间
let last = 0;
return function() {
// 保留调用时的this上下文
let context = this;
// 保留调用时传入的参数
let args = arguments;
// 记录本次触发回调的时间
let now = +new Date();
// 时间间隔大于指定时间阈值,则触发回调
if (now - last >= interval) {
last = now;
fn.apply(context, args);
}
}
}
const better_scroll = throttle(() => console.log('done'), 1000);
document.addEventListener('scroll', better_scroll);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 防抖
所谓“防抖”,就是在某段时间内,不管你触发了多少次回调,都只认最后一次。也就是说,一段时间内,以最后一个请求为准。
function debounce(fn, delay) {
let timer = null
return function() {
let context = this;
let args = arguments;
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
fn.apply(context, args)
}, delay)
}
}
const better_scroll = debounce(() => console.log('done'), 1000)
document.addEventListener('scroll', better_scroll);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 节流和防抖合体
// fn是我们需要包装的事件回调, delay是时间间隔的阈值
function throttle(fn, delay) {
// last为上一次触发回调的时间, timer是定时器
let last = 0, timer = null
// 将throttle处理结果当作函数返回
return function () {
// 保留调用时的this上下文
let context = this
// 保留调用时传入的参数
let args = arguments
// 记录本次触发回调的时间
let now = +new Date()
// 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值
if (now - last < delay) {
// 如果时间间隔小于我们设定的时间间隔阈值,则为本次触发操作设立一个新的定时器
clearTimeout(timer)
timer = setTimeout(function () {
last = now
fn.apply(context, args)
}, delay)
} else {
// 如果时间间隔超出了我们设定的时间间隔阈值,那就不等了,无论如何要反馈给用户一次响应
last = now
fn.apply(context, args)
}
}
}
// 用新的throttle包装scroll的回调
const better_scroll = throttle(() => console.log('触发了滚动事件'), 1000)
document.addEventListener('scroll', better_scroll)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 总结
节流是技能需要冷却,再次使用需要等待;防抖是打断技能释放,以最后一次释放为准。
# 总结
上面是阅读《前端性能优化原理与实践》小册后所总结的所有性能优化的知识点,希望对看到本文的你有所帮助。