前端性能优化不完全手册

7次阅读

共计 4324 个字符,预计需要花费 11 分钟才能阅读完成。

性能优化是一门大学问,本文仅对个人一些积累知识的阐述,欢迎下面补充。
抛出一个问题,从输入 url 地址栏到所有内容显示到界面上做了哪些事?

1. 浏览器向 DNS 服务器请求解析该 URL 中的域名所对应的 IP 地址;
2. 建立 TCP 连接(三次握手);
3. 浏览器发出读取文件 (URL 中域名后面部分对应的文件) 的 HTTP 请求,该请求报文作为 TCP 三次握手的第三个报文的数据发送给服务器;
4. 服务器对浏览器请求作出响应,并把对应的 html 文本发送给浏览器;
5. 浏览器将该 html 文本并显示内容;
6. 释放 TCP 连接(四次挥手);

上面这个问题是一个面试官非常喜欢问的问题,我们下面把这 6 个步骤分解,逐步细谈优化。
一、DNS 解析

DNS` 解析: 将域名解析为 ip 地址 , 由上往下匹配,只要命中便停止

走缓存
浏览器 DNS 缓存
本机 DNS 缓存
路由器 DNS 缓存
网络运营商服务器 DNS 缓存(80% 的 DNS 解析在这完成的)
递归查询

优化策略:尽量允许使用浏览器的缓存,能给我们节省大量时间。
二、TCP 的三次握手

SYN(同步序列编号)ACK(确认字符)

第一次握手:Client 将标志位 SYN 置为 1,随机产生一个值 seq=J,并将该数据包发送给 Server,Client 进入 SYN_SENT 状态,等 待 Server 确认。
第二次握手:Server 收到数据包后由标志位 SYN= 1 知道 Client 请求建立连接,Server 将标志位 SYN 和 ACK 都置为 1,ack=J+1,随机产生一个值 seq=K,并将该数据包发送给 Client 以确认连接请求,Server 进入 SYN_RCVD 状态。
第三次握手:Client 收到确认后,检查 ack 是否为 J +1,ACK 是否为 1,如果正确则将标志位 ACK 置为 1,ack=K+1,并将该数据包发送给 Server,Server 检查 ack 是否为 K +1,ACK 是否为 1,如果正确则连接建立成功,Client 和 Server 进入 ESTABLISHED 状态,完成三次握手,随后 Client 与 Server 之间可以开始传输数据了。

三、浏览器发送请求
优化策略:

1.HTTP 协议通信最耗费时间的是建立 TCP 连接的过程,那我们就可以使用 HTTP Keep-Alive,在 HTTP 早期,每个 HTTP 请求都要求打开一个 TCP socket 连接,并且使用一次之后就断开这个 TCP 连接。使用 keep-alive 可以改善这种状态,即在一次 TCP 连接中可以持续发送多份数据而不会断开连接。通过使用 keep-alive 机制,可以减少 TCP 连接建立次数,也意味着可以减少 TIME_WAIT 状态连接,以此提高性能和提高 http 服务器的吞吐率(更少的 tcp 连接意味着更少的系统内核调用
2. 但是,keep-alive 并不是免费的午餐, 长时间的 TCP 连接容易导致系统资源无效占用。配置不当的 keep-alive,有时比重复利用连接带来的损失还更大。所以,正确地设置 keep-alive timeout 时间非常重要。(这个 keep-alive_timout 时间值意味着:一个 http 产生的 tcp 连接在传送完最后一个响应后,还需要 hold 住 keepalive_timeout 秒后,才开始关闭这个连接),如果想更详细了解可以看这篇文章 keep-alve 性能优化的测试结果

3. 使用 webScoket 通信协议,仅一次 TCP 握手就一直保持连接,而且他对二进制数据的传输有更好的支持,可以应用于即时通信,海量高并发场景。webSocket 的原理以及详解

4. 减少 HTTP 请求次数,每次 HTTP 请求都会有请求头,返回响应都会有响应头,多次请求不仅浪费时间而且会让网络传输很多无效的资源,使用前端模块化技术 AMD CMD commonJS ES6 等模块化方案将多个文件压缩打包成一个,当然也不能都放在一个文件中,因为这样传输起来可能会很慢,权衡取一个中间值
5. 配置使用懒加载,对于一些用户不立刻使用到的文件到特定的事件触发再请求,也许用户只是想看到你首页上半屏的内容,但是你却请求了整个页面的所有图片,如果用户量很大,那么这是一种极大的浪费
6. 服务器资源的部署尽量使用同源策略

四、服务器返回响应,浏览器接受到响应数据
五、浏览器解析数据,绘制渲染页面的过程

先预解析(将需要发送请求的标签的请求发出去)
从上到下解析 html 文件
遇到 HTML 标签,调用 html 解析器将其解析 DOM 树
遇到 css 标记,调用 css 解析器将其解析 CSSOM 树

link 阻塞 – 为了解决闪屏,所有解决闪屏的样式

style 非阻塞,与闪屏的样式不相关的
将 DOM 树和 CSSOM 树结合在一起,形成 render 树
layout 布局 render 渲染
遇到 script 标签,阻塞,调用 js 解析器解析 js 代码,可能会修改 DOM 树,也可能会修改 CSSOM 树
将 DOM 树和 CSSOM 树结合在一起,形成 render 树

layout 布局 render 渲染(重排重绘)

script 标签的属性

async 异步 谁先回来谁就先解析,不阻塞
defer 异步 按照先后顺序(defer)解析,不阻塞
script 标签放在 body 下,放置多次重排重绘,能够操作 dom

性能优化策略:

需要阻塞的样式使用 link 引入,不需要的使用 style 标签(具体是否需要阻塞看业务场景)
图片比较多的时候,一定要使用懒加载,图片是最需要优化的,webpack4 中也要配置图片压缩,能极大压缩图片大小,对于新版本浏览器可以使用 webp 格式图片 webP 详解,图片优化对性能提升最大。

webpack4 配置 代码分割,提取公共代码成单独模块。方便缓存

/*
runtimeChunk 设置为 true, webpack 就会把 chunk 文件名全部存到一个单独的 chunk 中,
这样更新一个文件只会影响到它所在的 chunk 和 runtimeChunk,避免了引用这个 chunk 的文件也发生改变。
*/
runtimeChunk: true,
splitChunks: {
chunks: ‘all’ // 默认 entry 的 chunk 不会被拆分, 配置成 all, 就可以了
}
}
// 因为是单入口文件配置,所以没有考虑多入口的情况,多入口是应该分别进行处理。

对于需要事件驱动的 webpack4 配置懒加载的,可以看这篇 webpack4 优化教程, 写得非常全面
一些原生 javaScript 的 DOM 操作等优化会在下面总结

六、TCP 的四次挥手,断开连接

终结篇:性能只是 load 时间或者 DOMContentLoaded 时间的问题吗?

RAIL

Responce 响应,研究表明,100ms 内对用户的输入操作进行响应,通常会被人类认为是立即响应。时间再长,操作与反应之间的连接就会中断,人们就会觉得它的操作有延迟。例如:当用户点击一个按钮,如果 100ms 内给出响应,那么用户就会觉得响应很及时,不会察觉到丝毫延迟感。

Animaton 现如今大多数设备的屏幕刷新频率是 60Hz,也就是每秒钟屏幕刷新 60 次;因此网页动画的运行速度只要达到 60FPS,我们就会觉得动画很流畅。

Idle RAIL 规定,空闲周期内运行的任务不得超过 50ms,当然不止 RAIL 规定,W3C 性能工作组的 Longtasks 标准也规定了超过 50 毫秒的任务属于长任务,那么 50ms 这个数字是怎么得来的呢?浏览器是单线程的,这意味着同一时间主线程只能处理一个任务,如果一个任务执行时间过长,浏览器则无法执行其他任务,用户会感觉到浏览器被卡死了,因为他的输入得不到任何响应。为了达到 100ms 内给出响应,将空闲周期执行的任务限制为 50ms 意味着,即使用户的输入行为发生在空闲任务刚开始执行,浏览器仍有剩余的 50ms 时间用来响应用户输入,而不会产生用户可察觉的延迟。

Load 如果不能在 1 秒钟内加载网页并让用户看到内容,用户的注意力就会分散。用户会觉得他要做的事情被打断,如果 10 秒钟还打不开网页,用户会感到失望,会放弃他们想做的事,以后他们或许都不会再回来。

如何使网页更丝滑?

使用 requestAnimationFrame
即便你能保证每一帧的总耗时都小于 16ms,也无法保证一定不会出现丢帧的情况,这取决于触发 JS 执行的方式。假设使用 setTimeout 或 setInterval 来触发 JS 执行并修改样式从而导致视觉变化;那么会有这样一种情况,因为 setTimeout 或 setInterval 没有办法保证回调函数什么时候执行,它可能在每一帧的中间执行,也可能在每一帧的最后执行。所以会导致即便我们能保障每一帧的总耗时小于 16ms,但是执行的时机如果在每一帧的中间或最后,最后的结果依然是没有办法每隔 16ms 让屏幕产生一次变化,也就是说,即便我们能保证每一帧总体时间小于 16ms,但如果使用定时器触发动画,那么由于定时器的触发时机不确定,所以还是会导致动画丢帧。现在整个 Web 只有一个 API 可以解决这个问题,那就是 requestAnimationFrame,它可以保证回调函数稳定的在每一帧最开始触发。

避免 FSL

先执行 JS,然后在 JS 中修改了样式从而导致样式计算,然后样式的改动触发了布局、绘制、合成。但 JavaScript 可以强制浏览器将布局提前执行,这就叫 强制同步布局 FSL。
// 读取 offsetWidth 的值会导致重绘
const newWidth = container.offsetWidth;

// 设置 width 的值会导致重排,但是 for 循环内部
代码执行速度极快,当上面的查询操作导致的重绘
还没有完成,下面的代码又会导致重排,而且这个重
排会强制结束上面的重绘,直接重排,这样对性能影响
非常大。所以我们一般会在循环外部定义一个变量,这里
面使用变量代替 container.offsetWidth;
boxes[i].style.width = newWidth + ‘px’;
}

使用 transform 属性去操作动画,这个属性是由合成器单独处理的,所以使用这个属性可以避免布局与绘制。
使用 translateZ(0)开启图层,减少重绘重排。特别在移动端,尽量使用 transform 代替 absolute。创建图层的最佳方式是使用 will-change,但某些不支持这个属性的浏览器可以使用 3D 变形(transform: translateZ(0))来强制创建一个新层。
有兴趣的可以看看这篇文字 前端页面优化

样式的切换最好提前定义好 class,通过 class 的切换批量修改样式,避免多次重绘重排
可以先切换 display:none 再修改样式
多次的 append 操作可以先插入到一个新生成的元素中,再一次性插入到页面中。

以上都是根据本人的知识点总结得出,后期还会有 React 的性能优化方案等出来,路过点个赞收藏收藏~,欢迎提出问题补充~

正文完
 0