性能优化是一门大学问,本文仅对个人一些积累知识的阐述,欢迎下面补充。
抛出一个问题,从输入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.服务器资源的部署尽量使用同源策略
四、服务器返回响应,浏览器接受到响应数据
一直没想到这里使用什么优化手段,今晚想到了,使用Nginx反向代理服务器,主要是对服务器端的优化。
- Nginx是一款轻量级的Web 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器,并在一个BSD-like 协议下发行。其特点是占有内存少,并发能力强,事实上nginx的并发能力确实在同类型的网页服务器中表现较好,中国大陆使用nginx网站用户有:百度、京东、新浪、网易、腾讯、淘宝等。
- Nginx 是一个安装非常的简单、配置文件非常简洁(还能够支持perl语法)、Bug非常少的服务。Nginx 启动特别容易,并且几乎可以做到7*24不间断运行,即使运行数个月也不需要重新启动。你还能够不间断服务的情况下进行软件版本的升级。
- 它可以:解决跨域,请求过滤,配置gzip,负载均衡,静态资源服务器 等...
- 把服务窗口想像成我们的后端服务器,而后面终端的人则是无数个客户端正在发起请求。负载均衡就是用来帮助我们将众多的客户端请求合理的分配到各个服务器,以达到服务端资源的充分利用和更少的请求时间。
- Nginx如何实现负载均衡
- nginx如何实现负载均衡
- Upstream指定后端服务器地址列表
- upstream balanceServer {
- server 10.1.22.33:12345;
- server 10.1.22.34:12345;
- server 10.1.22.35:12345;
- }
- 复制代码在server中拦截响应请求,并将请求转发到Upstream中配置的服务器列表。
- server {
- server_name fe.server.com;
- listen 80;
- location /api {
- proxy_pass http://balanceServer;
- }
- }
- 上面的配置只是指定了nginx需要转发的服务端列表,并没有指定分配策略。
- 默认情况下采用的策略,将所有客户端请求轮询分配给服务端。这种策略是可以正常工作的,但是如果其中某一台服务器压力太大,出现延迟,会影响所有分配在这台服务器下的用户。
- 最小连接数策略
将请求优先分配给压力较小的服务器,它可以平衡每个队列的长度,并避免向压力大的服务器添加更多的请求。
- upstream balanceServer {
- least_conn; //配置压力较小的服务器
- server 10.1.22.33:12345;
- server 10.1.22.34:12345;
- server 10.1.22.35:12345;
- }
- 依赖于NGINX Plus,优先分配给响应时间最短的服务器。
- upstream balanceServer {
- fair; //配置响应时间最短的服务器
- server 10.1.22.33:12345;
- server 10.1.22.34:12345;
- server 10.1.22.35:12345;
- }
- 客户端ip绑定
- 来自同一个ip的请求永远只分配一台服务器,有效解决了动态网页存在的session共享问题。
- upstream balanceServer {
- ip_hash; //配置1个IP永远只分配一台服务器
- server 10.1.22.33:12345;
- server 10.1.22.34:12345;
- server 10.1.22.35:12345;
- }
- 配置静态资源服务器
- location ~* \.(png|gif|jpg|jpeg)$ {
- root /root/static/;
- autoindex on;
- access_log off;
- expires 10h;# 设置过期时间为10小时
- }
- 复制代码匹配以png|gif|jpg|jpeg为结尾的请求,
- 并将请求转发到本地路径,root中指定的路径即nginx
- 本地路径。同时也可以进行一些缓存的设置
- Nginx解决跨域
- nginx解决跨域的原理
- 例如:
- 前端server的域名为:fe.server.com
- 后端服务的域名为:dev.server.com
- 现在我在fe.server.com对dev.server.com发起请求一定会出现跨域。
- 现在我们只需要启动一个nginx服务器,将server_name设置为fe.server.com,
- 然后设置相应的location以拦截前端需要跨域的请求,最后将请求代理回dev.server.com。
- 如下面的配置:
- server {
- listen 80;
- server_name fe.server.com;
- location / {
- proxy_pass dev.server.com;
- }
- }
- 复制代码这样可以完美绕过浏览器的同源策略:fe.server.com访问nginx的fe.server.com
- 属于同源访问,而nginx对服务端转发的请求不会触发浏览器的同源策略。
- 最重要的一点来了,现在的网站大都使用了这种配置:
- 配置GZIP
- GZIP是规定的三种标准HTTP压缩格式之一。目前绝大多数的网站都在使用GZIP传输 HTML、CSS、JavaScript 等资源文件。
- 对于文本文件,GZip 的效果非常明显,开启后传输所需流量大约会降至 1/4 ~ 1/3。
- 启用 GZip 所需的HTTP 最低版本默认值为HTTP/1.1
- 启用gzip同时需要客户端和服务端的支持,如果客户端支持gzip的解析,那么只要服务端能够返回gzip的文件就可以启用gzip了,我们可以通过nginx的配置来让服务端支持gzip。下面的respone中content-encoding:gzip,指服务端开启了gzip的压缩方式。
- 具体可以看这篇文字文章 Nginx配置GZIP
- 配置GZIP
对于文本文件,GZip 的效果非常明显,开启后传输所需流量大约会降至 1/4 ~ 1/3。
Nginx功能非常强大,配置也非常方便,有兴趣的可以多看看这篇文章 Nginx解析
五、浏览器解析数据,绘制渲染页面的过程
- 先预解析(将需要发送请求的标签的请求发出去)
- 从上到下解析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中封装成高阶组件,ES6中可以使用继承,TypeScript中接口继承,类继承,接口合并,类合并。
- 强力推荐阅读:阮一峰ES6教程
- 以及什么是TypeScript以及入门
以上都是根据本人的知识点总结得出,后期还会有更多性能优化方案等出来,路过点个赞收藏收藏~,欢迎提出问题补充~
下面加入React的性能优化方案:
- 在生命周期函数shouldComponentUpdate中对this.state和prev state进行浅比较,使用for-in循环遍历两者,
只要得到他们每一项值,只要有一个不一样就返回true,更新组件。
- 定义组件时不适用React.component , 使用PureComponent代替,这样React机制会自动在shouldComponentUpdate中进行浅比较,决定是否更新。
- 上面两条优化方案只进行浅比较,只对比直接属性的值,当然你还可以在上面加入this.props和prevprops的遍历比较,因为shouldComponentUpdate的生命周期函数自带这两个参数。如果props 和 state 的值比较复杂,那么可以使用下面这种方式去进行深比较。
- 解决:
- 保证每次都是新的值
- 使用 immutable-js 库,这个库保证生成的值都是独立的
- var map1 = Immutable.Map({ a: 1, b: 2, c: 3 });
- var map2 = map1.set('b', 50);
- map1.get('b'); // 2
- map2.get('b'); // 50
- 总结:使用以上方式,可以减少不必要的重复渲染。
- React的JSX语法要求必须包裹一层根标签,为了减少不必要的DOM层级,我们使用Fragment标签代替,这样渲染时候不会渲染多余的DOM节点,让DIFF算法更快遍历。
- 使用Redux管理全局多个组件复用的状态。
- React构建的是SPA应用,对SEO不够友好,可以选择部分SSR技术进行SEO优化。
- 对Ant-design这类的UI组件库,进行按需加载配置,从import Button from 'antd' 的引入方式,变成import {Button} from antd的方式引入。(类似Babel7中的runtime和polifill的区别).