# JS性能优化
# style标签样式的渲染
- style标签中的样式是由html解析器进行解析的
- 浏览器加载资源是异步的
- 页面style标签写的内部样式是异步解析的
# css阻塞
声明:只有link引入的外部css才能够产生阻塞
# style标签中的样式
- 由html解析器进行解析的;
- 不阻塞浏览器渲染(可能会产生闪屏现象);
- 不阻塞DOM解析;
# link引入的外部css样式(推荐)
- 由css解析器来进行解析的;
- 阻塞浏览器渲染(可以利用这种阻塞避免闪屏现象);
- 阻塞其后面的js语句的执行;
- 不阻塞DOM的解析;
# 优化核心理念:尽可能快的提高外部css加载速度
- 使用CDN节点进行外部资源加速;
- 对CSS进行压缩(利用webpack等打包工具);
- 减少HTTP请求数,将多个CSS文件进行合并;
- 优化样式表的代码;
# js阻塞
# 阻塞DOM解析
浏览器不知道后续脚本的内容,如果先去解析了下面的DOM,而随后的js删除了后面所有的DOM,那么浏览器就做了无用功,浏览器无法预估脚本里面具体做了什么操作,例如像document.write这种操作,索性全部停住,等脚本执行完了,浏览器再继续向下解析DOM。
# 阻塞页面渲染
js中也可以给DOM设置样式,浏览器同样等该脚本执行完毕,再继续干活,避免做无用功。
# 阻塞后续js的执行
维护依赖关系,例如:必须先引入jQuery再引入bootstrap
# css图层
浏览器在渲染一个页面时,会将页面分为很多个图层,图层有大有小,每个图层上有一个或多个节点。在渲染DOM的时候,浏览器所做的工作实际上是:
- 获取DOM后分割为多个图层
- 对每个图层的节点计算样式结果 (Recalculate style--样式重计算)
- 为每个节点生成图形和位置 (Layout--重排,回流)
- 将每个节点绘制填充到图层位图中 (Paint--重绘)
- 图层作为纹理上传至GPU
- 组合多个图层到页面上生成最终屏幕图像 (Composite Layers--图层重组)
# 图层创建的条件
Chrome浏览器满足以下任意情况就会创建图层:
- 拥有具有3D变化的CSS属性 例如
transform: translate3d()
或transform: translateZ()
- 使用
video
节点标签 - 使用
canvas
节点标签(canvas分为画布层和画笔层) - CSS动画的节点
@keyframes animation
- 拥有CSS加速属性的元素(will-change)
# 重绘(Repaint)
重绘是一个元素外观的改变所触发的浏览器行为,例如改变outline、背景色等属性。浏览器会根据元素的新属性重新绘制,使元素呈现新的外观。重绘不会带来重新布局,所以并不一定伴随重排。
需要注意的是:重绘是以图层为单位,如果图层中某个元素需要重绘,那么整个图层都需要重绘。
比如一个图层包含很多节点,其中有个gif图,gif图的每一帧,都会重回整个图层的其他节点,然后生成最终的图层位图。
所以这需要通过特殊的方式来强制gif图属于自己一个图层(translateZ(0)或者translate3d(0,0,0)
CSS3的动画也是一样(好在绝大部分情况浏览器自己会为CSS3动画的节点创建图层)
# 重排(Reflow 回流)
渲染对象在创建完成并添加到渲染树时,并不包含位置和大小信息。计算这些值的过程称为布局或重排
"重绘"不一定需要"重排",比如改变某个网页元素的颜色,就只会触发"重绘",不会触发"重排",因为布局没有改变。
但是,"重排"必然导致"重绘",比如改变一个网页元素的位置,就会同时触发"重排"和"重绘",因为布局改变了。
# 触发重绘的属性
* color * background * outline-color
* border-style * background-image * outline
* border-radius * background-position * outline-style
* visibility * background-repeat * outline-width
* text-decoration * background-size * box-shadow
# 触发重排(回流)的属性
盒子模型相关属性会触发重布局 | 定位属性及浮动也会触发重布局 | 改变节点内部文字结构也会触发重布局 |
---|---|---|
width | top | text-align |
height | bottom | overflow-y |
padding | left | font-weight |
margin | right | overflow |
display | position | font-family |
border-width | float | line-height |
border | clear | vertival-align |
min-height | white-space |
# 常见的触发重排的操作
Reflow 的成本比 Repaint 的成本高得多的多。DOM Tree 里的每个结点都会有 reflow 方法,一个结点的 reflow 很有可能导致子结点,甚至父点以及同级结点的 reflow。在一些高性能的电脑上也许还没什么,但是如果 reflow 发生在手机上,那么这个过程是非常痛苦和耗电的。
所以,下面这些动作有很大可能会是成本比较高的。
当你增加、删除、修改 DOM 结点时,会导致 Reflow , Repaint。
当你移动 DOM 的位置
当你修改 CSS 样式的时候。
当你 Resize 窗口的时候(移动端没有这个问题,因为移动端的缩放没有影响布局视口)
当你修改网页的默认字体时。
获取某些属性时(width,height...)
注:display:none 会触发 reflow,而 visibility:hidden 只会触发 repaint,因为没有发生位置变化。
# 重绘、回流优化方案
如果我们需要使得动画或其他节点渲染的性能提高,需要做的就是减少浏览器在运行时所需要做的工作(尽量减少1234步)
- 计算需要被加载到节点上的样式结果(Recalculate style--样式重计算)
- 为每个节点生成图形和位置(Layout--回流和重布局)
- 将每个节点填充到图层中(Paint Setup和Paint--重绘)
- 组合图层到页面上(Composite Layers--图层重组)
# 优化
- 元素位置移动变换时尽量使用CSS3的transform来代替对top left等的操作,变换(transform)和透明度(opacity)的改变仅仅影响图层的组合
- 使用opacity来代替visibility
- 使用visibility不触发重排,但是依然重绘。
- 直接使用opacity即触发重绘,又触发重排(GPU底层设计如此!)。
- opacity配合图层使用,即不触发重绘也不触发重排。
- 原因:透明度的改变时,GPU在绘画时只是简单的降低之前已经画好的纹理的alpha值来达到效果,并不需要整体的重绘。不过这个前提是这个被修改opacity本身必须是一个图层。
- 不要使用table布局
- table-cell
- 将多次改变样式属性的操作合并成一次操作,不要一条一条地修改DOM的样式,预先定义好class,然后修改DOM的className
- 将DOM离线后再修改,由于display属性为none的元素不在渲染树中,对隐藏的元素操作不会引发其他元素的重排。如果要对一个元素进行复杂的操作时,可以先隐藏它,操作完成后再显示。这样只在隐藏和显示时触发2次重排。
- 利用文档碎片(documentFragment)------vue使用了该种方式提升性能。
- 不要把获取某些DOM节点的属性值放在一个循环里当成循环的变量,当你请求向浏览器请求一些 style信息的时候,就会让浏览器flush队列,比如:
- offsetTop, offsetLeft, offsetWidth, offsetHeight
- scrollTop/Left/Width/Height
- clientTop/Left/Width/Height
- width,height
- 当你请求上面的一些属性的时候,浏览器为了给你最精确的值,需要刷新内部队列,因为队列中可能会有影响到这些值的操作。即使你获取元素的布局和样式信息跟最近发生或改变的布局信息无关,浏览器都会强行刷新渲染队列。
- 动画实现过程中,启用GPU硬件加速:transform: tranlateZ(0)
- 为动画元素新建图层,提高动画元素的z-index
# 函数防抖
函数防抖:
概念:延迟要执行的动作,若在延迟的这段时间内,再次触发,则取消之前开启的动作,重新计时
举例:电脑无操作一分钟后会进入休眠,当在第40s时鼠标被移动了一下,则重新计时一分钟
实现:定时器
应用:搜索时等用户完整输入完内容后再发送ajax查询请求
# 案例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>函数防抖</title>
</head>
<body>
<input type="text" id="user_input">
<script>
let input = document.querySelector("#user_input");
let timer;
input.addEventListener("keyup",(e) => {
let val = input.value;
clearTimeout(timer);
timer = setTimeout(() => {
mockAjax(val);
},1500)
});
function mockAjax(val) {
console.log("send------",val);
}
</script>
</body>
</html>
# 函数节流
函数节流
概念:设定一个特定的时间,让函数在特定的时间内只执行一次,不会频繁执行
举例:fps游戏,鼠标按住不松手,子弹也不会连成一条线
实现:定时器、标识
应用:在鼠标滚轮滚动的时候,每隔2s,打印一次
# 案例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>函数节流</title>
<style>
body {
height: 3000px;
}
</style>
</head>
<body>
<script>
let body = document.getElementsByTagName("body")[0];
let flag = true;
body.onscroll = () => {
if(flag) {
console.log(111);
flag = false;
setTimeout(() => {
flag = true;
},2000)
}
};
</script>
</body>
</html>
# requestAnimationFrame
window.requestAnimationFrame() 请求动画帧
1.window.requestAnimationFrame()
说明:该方法会告诉浏览器在重绘之前调用你所指定的函数
1.参数:该方法使用一个回调函数作为参数,这个回调函数会在浏览器重绘之前调用。
回调函数会被自动传入一个参数,DOMHighResTimeStamp,标识requestAnimationFrame()开始触发回调函数的当前时间
2.返回值:
一个long整数,也成为请求 ID,是个非零值 ,是回调列表中唯一的标识,没别的意义。
2.window.cancelAnimationFrame(requestID)
取消一个先前通过调用window.requestAnimationFrame()方法添加到计划中的动画帧请求。
requestID是先前调用window.requestAnimationFrame()方法时返回的ID.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>定时器动画</title>
<style>
#box {
position: absolute;
left: 10px;
top: 10px;
width: 200px;
height: 200px;
background-color: red;
}
</style>
</head>
<body>
<div id="box"></div>
<script>
// let i = 0;
// setInterval(() => {
// i+=.5;
// document.getElementById("box").style.transform = `translateX(${i}px)`;
// },10)
let i = 0;
let timer = requestAnimationFrame(move);
function move() {
i++;
document.getElementById("box").style.transform = `translateX(${i}px)`;
timer = requestAnimationFrame(move);
}
setTimeout(() => {
cancelAnimationFrame(timer);
},2000)
</script>
</body>
</html>
# CDN
网站通常将其所有的服务器都放在同一个地方,当用户群增加时,公司就必须在多个地理位置不同的服务器上部署内容,为了缩短http请求的时间,我们应该把大量的静态资源放置的离用户近一点。
# 内容发布网络CDN(Content Delivery Networks)
CDN是一组分布在多个不同地理位置的web服务器,用于更加有效的向用户发布内容
# 基本思路:
尽可能避开互联网上有可能影响数据传输速度和稳定性的瓶颈和环节,使内容传输的更快、更稳定。通过在网络各处放置节点服务器所构成的在现有的互联网基础之上的一层智能虚拟网络,CDN系统能够实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息,将用户的请求重新导向离用户最近的服务节点上。
基础架构:最简单的CDN网络由一个DNS服务器和几台缓存服务器组成
1.当用户点击网站页面上的内容URL,经过本地DNS系统解析,DNS系统会最终将域名的解析权交给CNAME指向的CDN专用DNS服务器。
2.CDN的DNS服务器将CDN的全局负载均衡设备IP地址返回用户。
3.用户向CDN的全局负载均衡设备发起内容URL访问请求。
4.CDN全局负载均衡设备根据用户IP地址,以及用户请求的内容URL,
选择一台用户所属区域的区域负载均衡设备,告诉用户向这台设备发起请求。
5.区域负载均衡设备会为用户选择一台合适的缓存服务器提供服务,
选择的依据包括:根据用户IP地址,判断哪一台服务器距用户最近;
根据用户所请求的URL中携带的内容名称,判断哪一台服务器上有用户所需内容;
查询各个服务器当前的负载情况,判断哪一台服务器尚有服务能力。
基于以上这些条件的综合分析之后,
区域负载均衡设备会向全局负载均衡设备返回一台缓存服务器的IP地址。
6.全局负载均衡设备把服务器的IP地址返回给用户。
7.用户向缓存服务器发起请求,缓存服务器响应用户请求,将用户所需内容传送到用户终端。
如果这台缓存服务器上并没有用户想要的内容,而区域均衡设备依然将它分配给了用户,
那么这台服务器就要向它的上一级缓存服务器请求内容,直至追溯到网站的源服务器将内容拉到本地。
# CDN大白话
以前呢,人们在网上买一件产品,买家付款之后,卖家确认发货… 但是如果这个卖家是在北京,买家是在广州,那么卖家就要把北京的货发到广州,这个可能需要2~3天的时间。
但是,如果这个卖家,首先在武汉设立了一个物流的转运中心,先把自己的货提前囤到当地的仓库,那么现在如果有一个广州的买家拍卖了一件商品,卖家不需要从北京发货,而是可以直接从武汉发货,可能就是一天的时候,买家就能收到货,这样时效性就大大提高!
# 缓存
# 1. 缓存理解
1. 缓存定义:
1. 浏览器在本地磁盘上将用户之前请求的数据存储起来,当访问者再次需要改数据的时候无需再次发送请求,直接从浏览器本地获取数据
2. 缓存的好处:
1. 减少请求的个数
2. 节省带宽,避免浪费不必要的网络资源
3. 减轻服务器压力
4. 提高浏览器网页的加载速度,提高用户体验
# 2. 缓存分类
1. 强缓存
1. 不会向服务器发送请求,直接从本地缓存中获取数据
2. 请求资源的的状态码为: 200 ok(from memory cache)
2. 协商缓存
1. 向服务器发送请求,服务器会根据请求头的资源判断是否命中协商缓存
2. 如果命中,则返回304状态码通知浏览器从缓存中读取资源
3. 强缓存 & 协商缓存的共同点
1. 都是从浏览器端读取资源
4. 强缓存 VS 协商缓存的不同点
1. 强缓存不发请求给服务器
2. 协商缓存发请求给服务器,根据服务器返回的信息决定是否使用缓存
# 3. 缓存使用示意图
# 4. 缓存中的header参数
# 1、强缓存的header参数
# unix时间戳是从1970年1月1日(UTC/GMT的午夜)开始到目前所经过的秒数,
- expires:
- 这是http1.0时的规范;它的值为一个绝对时间的GMT(格林威治标准时间)格式的时间字符串,如
Mon, 10 Jun 2015 21:31:12 GMT
,如果发送请求的时间在expires之前,那么本地缓存始终有效,否则就会发送请求到服务器来获取资源
- 这是http1.0时的规范;它的值为一个绝对时间的GMT(格林威治标准时间)格式的时间字符串,如
- cache-control:max-age=number
- 这是http1.1时出现的header信息,主要是利用该字段的max-age值来进行判断,它是一个相对值;资源第一次的请求时间和Cache-Control设定的有效期,计算出一个资源过期时间,再拿这个过期时间跟当前的请求时间比较,如果请求时间在过期时间之前,就能命中缓存,否则就不行;
- no-cache: 不使用本地缓存,需要使用协商缓存。先与服务器确认返回的响应是否被更改,如果之前的响应中存在Etag,那么请求的额时候会与服务器端进行验证,如果资源为被更改则使用缓存。
- no-store: 直接禁止游览器缓存数据,每次用户请求该资源,都会向服务器发送一个请求,每次都会下载完整的资源。
- public:可以被所有的用户缓存,包括终端用户和CDN等中间代理服务器。
- private:只能被终端用户的浏览器缓存,不允许CDN等中继缓存服务器对其缓存。
- 注意:当cache-control与Expires共存的时候cache-control的优先级高
# 2、协商缓存的header参数
重点:协商缓存都是由服务器来确定缓存资源是否可用的,所以客户端与服务器端要通过某种标识来进行通信,从而让服务器判断请求资源是否可以缓存访问
Last-Modified/If-Modified-Since:二者的值都是GMT格式的时间字符串
- 浏览器第一次跟服务器请求一个资源,服务器在返回这个资源的同时,在respone的header加上Last-Modified的header,这个header表示这个资源在服务器上的最后修改时间
- 浏览器再次跟服务器请求这个资源时,在request的header上加上If-Modified-Since的header,这个header的值就是上一次请求时返回的Last-Modified的值
- 服务器再次收到资源请求时,根据浏览器传过来If-Modified-Since和资源在服务器上的最后修改时间判断资源是否有变化,如果没有变化则返回304 Not Modified,但是不会返回资源内容;如果有变化,就正常返回资源内容。当服务器返回304 Not Modified的响应时,response header中不会再添加Last-Modified的header,因为既然资源没有变化,那么Last-Modified也就不会改变,这是服务器返回304时的response header
- 浏览器收到304的响应后,就会从缓存中加载资源
- 如果协商缓存没有命中,浏览器直接从服务器加载资源时,Last-Modified的Header在重新加载的时候会被更新,下次请求时,If-Modified-Since会启用上次返回的Last-Modified值
- 图例:
- Etag/If-None-Match
- 这两个值是由服务器生成的每个资源的唯一标识字符串,只要资源有变化就这个值就会改变
- 其判断过程与Last-Modified/If-Modified-Since类似
- 既生Last-Modified何生Etag
- HTTP1.1中Etag的出现主要是为了解决几个Last-Modified比较难解决的问题
- 一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET
- 某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒);
- 某些服务器不能精确的得到文件的最后修改时间。
- 小结:
利用Etag能够更加准确的控制缓存,因为Etag是服务器自动生成或者由开发者生成的对应资源在服务器端的唯一标识符。
Last-Modified与ETag是可以一起使用的,服务器会优先验证ETag,一致的情况下,才会继续比对Last-Modified,最后才决定是否返回304。
# 5. 强缓存如何重新加载新的资源
- 通过更新页面中引用的资源路径,让浏览器主动放弃加载缓存去加载新的资源
- 示例:
https://www.baidu.com/s?t=7aec0h3KB3Ba8lAbuyPg0AC0eDa59IvtDSmtMQBc6eW
- 好处:
- 每次文件改变后query的值就会发生修改,当query值不同的时候也就是页面引用的资源路径不同。此时浏览器会主动加载新的资源。