61

前端性能优化不完全手册 【已更新至React】

 5 years ago
source link: http://developer.51cto.com/art/201904/594934.htm?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

2u2ae2Q.jpg!web

性能优化是一门大学问,本文仅对个人一些积累知识的阐述,欢迎下面补充。

抛出一个问题,从输入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中封装成高阶组件,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的区别).

【责任编辑:庞桂玉 TEL:(010)68476606】


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK