前端网络安全&性能优化

1,112 阅读28分钟

一、网络安全

参考资料:

  1. Redos测试网站
  2. 正则表达式的攻击原理

1、浏览器原理

1、核心概念

进程
  • 1. Browser进程

    • 负责浏览器界⾯显示,与⽤户交互。如前进,后退等;
    • 负责各个⻚⾯的管理,创建和销毁其他进程;
    • 将Renderer进程得到的内存中的Bitmap,绘制到⽤户界⾯上;
    • ⽹络资源的管理,下载等。
  • 2. 第三方插件进程

    • 每种类型的插件对应⼀个进程,仅当使⽤该插件时才创建。
  • 3. GPU进程

    • 最多⼀个,⽤于3D绘制,硬件加速。
  • 4. 浏览器渲染进程

    浏览器渲染进程,也被称为浏览器内核。负责页面的渲染,脚本执行,时间处理,网络请求等功能。分类有:

    • Google Chrome:Chrome 28开发版本中还在使用WebKit,最新的Chrome使用Blink。
    • Internet Explorer:Trident内核,ie内核
    • Mozilla Firefox:Gecko内核,firefox内核
    • safari:Webkit
    • Opera:最初是Presto内核,后来是Webkit,现在是Blink内核

图片.png

线程
  • 1. GUI渲染线程

    • 负责渲染浏览器界⾯,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等;
    • 当界⾯需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执⾏;
    • 注意,GUI渲染线程与JS引擎线程是互斥的,当JS引擎执⾏时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在⼀个队列中等到JS引擎空闲时⽴即被执⾏
  • 2. JS引擎线程

    • 也称为JS内核,负责处理Javascript脚本程序;(例如V8引擎)
    • JS引擎线程负责解析Javascript脚本,运⾏代码;
    • JS引擎⼀直等待着任务队列中任务的到来,然后加以处理,⼀个Tab⻚(renderer进程)中⽆论什么时候都只有⼀个JS线程在运⾏JS程序;
    • 同样注意,GUI渲染线程与JS引擎线程是互斥的,所以如果JS执⾏的时间过⻓,这样就会造成⻚⾯的渲染不连贯,导致⻚⾯渲染加载阻塞。
  • 3. 事件触发线程

    • 归属于浏览器⽽不是JS引擎,⽤来控制事件循环(可以理解,JS引擎⾃⼰都忙不过来,需要浏览器另开线程协助);
    • 当JS引擎执⾏代码块如setTimeOut时(也可来⾃浏览器内核的其他线程,如⿏标点击、AJAX异步请求等),会将对应任务添加到事件线程中
    • 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理;
    • 注意,由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执⾏)。
  • 4. 定时触发器线程

    • 传说中的setInterval与setTimeout所在线程;
    • 浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确);
    • 因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执⾏);
    • 注意,W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms
  • 5. 异步http请求线程

    • 在XMLHttpRequest在连接后是通过浏览器新开⼀个线程请求;
    • 将检测到状态变更时,如果设置有回调函数,异步线程就产⽣状态变更事件,将这个回调再放⼊事件队列中。再由JavaScript引擎执⾏。

2、Browser进程与浏览器渲染进程通信

  • 1、Browser进程收到⽤户请求,⾸先需要获取⻚⾯内容(譬如通过⽹络下载资源),随后将该任务通过RendererHost接⼝传递给Render进程;
  • 2、Renderer进程的Renderer接⼝收到消息,简单解释后,交给渲染线程,然后开始渲染
    • 渲染线程接收请求,加载⽹⻚并渲染⽹⻚,这其中可能需要Browser进程获取资源和需要GPU进程来帮助渲染;
    • 当然可能会有JS线程操作DOM(这样可能会造成回流并重绘);
    • 最后Render进程将结果传递给Browser进程。
  • 3、Browser进程接收到结果并将结果绘制出来。

3、结构

WeChatf228f75bed42201d43f35dd4e9d3fcca.png

4、浏览器渲染过程

浏览器.png 在一个进程中,至少有一个线程,线程为CPU任务调度的最小执行单位。浏览器的渲染进程中有如下线程:

  1. GUI渲染线程:解析html文档,生成DOM树与CSS树(注意CSS树不会阻塞dom树的生成)。当生成DOM树和CSS树之后,就根据这两个树生成一个render树(在生成render树时候,如果有一方没有解析完毕就会等待解析完成,此时双方会相互阻塞),然后将这个render树渲染到界面上。当页面触发回流或者重绘的时候,会再次执行此次操作。
  2. js线程:执行js代码。与GUI线程互斥,当一个执行时,另一个会被强制挂起。当js执行一个时间负责度高的算法时,导致GUI渲染线程被挂起太久,会导致页面卡顿,解决办法可以通过web worker解决。
  3. 定时器线程:用来处理定时器线程,当定时器到期的时候,将回调放在任务队列里面,等待js线程的执行。js是单线程的,为了避免代码执行冲突,添加定时器线程,用于处理定时器操作。
  4. 事件触发线程:用来管理事件的触发,例如:点击事件、鼠标移动事件。当这些事件被触发时,将事件的回调添加至任务队列,等待js的执行。
  5. 异步HTTP请求线程:在XMLHttpRequest连接后新启动一个线程,线程如果检测到请求的状态变更,如果设置有回调函数,该线程会把回调函数添加至事件队列,等待js执行。
1、渲染流程

构建 DOM 树 -> CSS Parser -> 样式计算 -> 布局阶段 -> 分层 ->绘制 -> 分块 -> 光栅化和合成,渲染是在渲染进程执⾏的,渲染进程分为渲染主线程、光栅线程、合成线程等;从分块阶段开始,分块、光栅化、合成这三步是在⾮渲染主线程执⾏。

⼀个完整的渲染流程⼤致可总结为如下:

  1. 构建 DOM 树: 渲染进程通过 HTML Parser 将 HTML 字符串转换为浏览器能够读懂的 DOM 树结构,查看:document
  2. 构建 Style Rules: 渲染引擎将 CSS 字符串转化为浏览器可以理解的 styleSheets,查看:document.styleSheets
  3. 样式计算:根据上⾯两步⽣成的 DOM Tree 和 Style Rules,可以计算出 DOM 节点的样式。
  4. 创建布局树:计算出 DOM 节点的样式后,还需要知道 DOM 节点在⻚⾯中显示的位置,这就需要计算元素的布局信息,⽣成⼀颗布局树,其中不包括不显示的 DOM 节点。
  5. 分层:对布局树进⾏分层,并⽣成分层树,具体哪些元素需要分层是由 CSS 决定的,⽐如 z-index等,也可以设置 will-change 来通知浏览器此元素需要分层。
  6. 绘制:为每个图层⽣成绘制列表,并将其提交到合成线程。
  7. 分块、光栅化: 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。
  8. 合成:把光栅化⽣成的多张位图合成⼀张位图;合成线程发送绘制图块命令 DrawQuad 给浏览器进程。
  9. 显示:浏览器进程根据 DrawQuad 消息⽣成⻚⾯,并显示到显示器上。
2、相关问题解析
  1. HTML Parser需要等待HTML全部加载完再解析?还是边加载边解析?

    一边加载一边解析,转换为:document

  2. JS如何影响HTML Parser?

    script脚本放在body元素中页面内容的后面,避免js阻碍html解析,减少白屏时间,

    • 内嵌的js脚本会执行阻塞html解析、网络加载的js脚本也会阻塞html解析
    • script标签中的defer属性,延迟执行脚本,解析完</html>后执行,执行顺序不变,即第一个defer脚本会在第二个defer脚本前执行
    • script标签中的async属性,异步执行脚本,文件加载完成后执行,无法保证执行顺序,哪个加载完哪个执行。
  3. css如何影响js执行?

    由于JS可以获取和操作样式,因此会等待JS语句之前的CSS Parser完成后才会执⾏此JS⽂件。 结论: CSS Parser -> JS执⾏ -> HTML Parser。

  4. css的渲染?

    计算样式的过程不会等待⻚⾯的底部的Style rules⽣成完毕, 会先paint⼀次⻚⾯,等⻚⾯底部的CSS解析完成后,repaint⼀次,会看到闪烁的情况;因此CSS⽂件尽量放在head中,尽快下载CSS⽂件并解析,然后结合dom渲染到⻚⾯中

  5. 性能优化方法:下载CSS⽂件并解析、下载JS⽂件并执⾏可能会影响到⻚⾯的⾸屏渲染。因此⼀般可采⽤如下策略:

    • css文件加载尽量放在head中,减少CSS⽂件体积,对于⼤的CSS⽂件,可以通过媒体查询的⽅式加载不同⽤途的CSS⽂件, 参考
    • 动画采⽤css3动画,或者添加will-change属性
    • script标签放在⻚⾯内容后,或者不需要在HTML Parser阶段使⽤的,可以加上defer 或者 async
    • ⽹络层⾯:DNS prefetch; 采⽤CDN;开启HTTP2等

2、HTTP缓存

WeChatba5f1de0d4e48911f31e6077c7325dc3.png

3、WEB安全

  • XSS、CSRF、HTTPS

  • csp:内容安全策略,可以禁止加载外域代码,禁止外域的提交,Content-Security-Policy|X-XSS-Protection

  • HSTS:强制客户端使用HTTPS与服务端建立连接

  • X-Frame-Options:控制当前页面是否可以别嵌入到iframe中

    • DENY:表示该页面不允许在frame中展示,即便是在相同域名的页面中嵌套也不允许
    • SAMEORIGIN:表示该页面可以在相同域名页面的frame中展示
    • ALLOW-FROM uri:表示该页面可以在指定来源的frame中展示
  • SRI(subresource intergrity 子资源的完整性):请求cdn子资源的完整性

    • jian-xdfs-da-daaa.js:注入到index.html,并且上传至cdn
    • 用户在请求的时候,根据jian-xdfs-da-daaa.js去请求,而这个文件可能被篡改
    • 打包的时候根据js文件内容生成一个hash值,并且把hash值作为intergrity属性注入到script上,前端可以⽤webpack-subresource-integrity插件实现。
  • Referer-Policy:控制referer的携带策略

1、XSS

1、概念

Cross Site Script,XSS垮站脚本攻击,攻击者想尽一切办法把可执行代码注入到网页中。

2、攻击类型

外在表现上,都有哪些攻击场景:

  1. 评论区植入js代码(即可输入的地方)
  2. url上拼接js代码

技术角度上,有哪些类型的xss攻击:

  1. 存储型Server:服务端存在数据库

    论坛发帖,商品评价,用户私信等这些用户保存数据的网站。

    攻击步骤:

    • 攻击者将恶意代码提交到目标网站的数据库中
    • 用户打开目标网站的时候,服务端将评论(恶意代码)从数据库中取出,拼接到html返回给浏览器
    • 用户浏览器收到html后,混在其中的恶意代码就会执行
    • 窃取用户数据,发放到攻击者网站
  2. 反射型Server:拼接在url上

    攻击者结合各种手段,诱导用户点击恶意url,通过url传参数的功能,比如网站的搜索或跳转等。

    攻击步骤:

    • 攻击者构造出自己的恶意url
    • 直接执行可执行的恶意代码
  3. Dom型 Browser

    取出和执行恶意代码的操作,由浏览器完成,属于前端 JavaScript ⾃身的安全漏洞,⽽其他两种 XSS 都属于服务端的安全漏洞。

    攻击步骤:

    • 攻击者构造出特殊的 URL,其中包含恶意代码。
    • ⽤户打开带有恶意代码的 URL。
    • ⽤户浏览器接收到响应后解析执⾏,前端 JavaScript 取出 URL 中的恶意代码并执⾏。
    • 恶意代码窃取⽤户数据并发送到攻击者的⽹站,或者冒充⽤户的⾏为,调⽤⽬标⽹站的接⼝执⾏攻击者指定的操作。
3、防范攻击

主旨:防止攻击者提交恶意代码,防止浏览器执行恶意代码。

  1. 对数据进行严格的输入编码:

    • html元素的编码、js编码、css编码、url编码等;
    • 避免拼接html,eg:Vue/React技术栈,避免使用v-html/dangerouslyHtml。
  2. CSP:http header,即Content-Security-Policy,旧浏览器可以设置X-XSS-Protection

    • default-src 'self':所有内容均来⾃站点的同⼀个源(不包括其⼦域名)
    • default-src ‘self’ *.trusted.com:允许内容来⾃信任的域名及其⼦域名 (域名不必须与CSP设置所在的域名相同)
    • default-src https: //baidu.com :该服务器仅允许通过HTTPS⽅式并仅从lubai.com域名来访问⽂档
  3. 输入验证:phone,url,电话号码,邮箱做校验判断。

  4. 开启浏览器的XSS防御:http only Cookie,禁止js读取cookie值,完成xss注入也无法窃取cookie,eg:set-cookie,httponly。

  5. 验证码。

2、CSRF

1、概念

Cross-Site request forgery,跨站请求伪造:攻击者诱导受害者进⼊恶意⽹站,在第三⽅⽹站中,向被攻击⽹站发送跨站请求。利⽤受害者在被攻击⽹站已经获取的注册凭证,绕过后台的⽤户验证,达到冒充⽤户对被攻击的⽹站执⾏某项操作的⽬的。

攻击步骤:

  1. 受害者登录a.com,并且保留了登录凭证cookie
  2. 攻击者诱导受害者访问b.com
  3. b.com向a.com发送请求,a.com/xxxx,浏览器就会直接带上a.com的cookie
  4. a.com收到了请求,验证通过后,执行相应操作
  5. 攻击完成,攻击者在受害者不知情的情况下,冒充受害者,让a.com执行了自己定义的操作
2、攻击类型
  1. GET型:如在⻚⾯的某个 img 中发起⼀个 get 请求
  2. POST型:通过⾃动提交表单到恶意⽹站
3、如何防范

CSRF一般发生在第三方域名,攻击者无法获取到cookie信息,只是利用浏览器机制使用cookie

  1. 同源策略

    • 通过检查request header中的origin、referer、host等,判断请求站点是否是自己允许的站点。

      • host:任何请求携带,域名和端口号
      • origin:一般存在于跨域请求中,协议和域名,和Access-Control-Allow-Origin一起存在
      • referer:告知服务器原始url,其用于所有类型的请求,并且包括:协议+域名+查询参数
    • Referrer-Policy:可设置携带referer头,eg:Referrer-Policy: no-referrer|same-origin等。

  2. Cookie SameSite

    • Strict:完全禁止第三方cookie,⽐如a.com的⻚⾯中访问 b.com 的资源,那么a.com中的cookie不会被发送到 b.com服务器,只有从b.com的站点去请求b.com的资源,才会带上这些Cookie
    • Lax:在跨站点的情况下,从第三⽅站点链接打开和从第三⽅站点提交 Get⽅式的表单这两种⽅式都会携带Cookie。但如果在第三⽅站点中使⽤POST⽅法或者通过 img、Iframe等标签加载的URL,这些场景都不会携带Cookie
    • None:任何情况下都会发送 Cookie数据
  3. CSRF Token:提交请求时携带额外信息

    • 用户打开页面的时候,服务器生成一个token
    • 每次页面加载的时候,加到所有的能发请求的html元素上, ⽐如form, a
    • 每次前端发起请求, 都携带Token参数
    • 服务端每次接收请求, 都校验Token的有效性

4、node相关

  • 本地文件操作相关,路径拼接导致的文件泄露
  • ReDos
  • 时序攻击
  • ip origin referrer等request headers的校验

1、文件泄露

问题描述:静态文件服务,用户可通过输入路径获取私密文件

解决方法:

  • express:static
  • koa:中间件koa-static
  • npm包:resolve-path

2、ReDos

问题描述:正则表达式攻击,服务器经常会有正则去匹配⼀些传⼊的参数, 所以攻击者就可以利⽤正在表达式的这个特性, 来⼀直占⽤服务器运算资源, 造成服务器宕机。

正则表达式⼀般情况下会去匹配第⼀种可能性,直接匹配到最后发现成功了, 耗时就很短;每当⼀次匹配不成功, 就会尝试回溯到上⼀个字符, 看看能不能有其他的组合来匹配到这个字符串.

3、时序攻击

根据服务器的响应时间来碰撞出realArray的值。

二、性能优化

参考资料:

  1. LIGHTHOUSE 工具
  2. 前端性能优化指标

1、前置知识

输入url到页面最终呈现整体流程梳理:

  • url解析:判断输入是关键字搜索还是url访问,对url进行解析;

  • dns域名解析获取ip地址

    • 缓存查找:浏览器缓存(chrome://net-internals/#dns地址查看)、系统缓存(hosts)、路由器缓存、isp缓存
    • 向本地DNS服务器发送查询报文"query zh.wikipedia.org"
    • 本地DNS服务器检查自身缓存,存在返回,不存在向根域名服务器发送查询报文"query zh.wikipedia.org",得到顶级域 .org 的顶级域名服务器地址
    • DNS服务器向 .org 域的顶级域名服务器发送查询报文"query zh.wikipedia.org",得到二级域 .wikipedia.org 的权威域名服务器地址
    • DNS服务器向 .wikipedia.org 域的权威域名服务器发送查询报文"query zh.wikipedia.org",得到主机 zh 的A记录,存入自身缓存并返回给客户端
  • 使用IP建立TCP链接(三次握手)

    • 第一次握手: 建立连接时,客户端发送SYN标记的数据包(syn=j)到服务器,并进入SYN_SENT状态,等待服务器确认;
    • 第二次握手: 服务器收到SYN标记的数据包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
    • 第三次握手: 客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。
  • 发送http请求,服务器响应,缓存判断(强缓存和协商缓存)

    • 请求:发送命令+发送请求头信息+空白行+请求体(post)
    • 响应:响应状态 + 响应头+空白行+响应体
    • 强缓存:cache-control(max-age)、Expires
    • 协商缓存:返回Etag、Last-modified和请求IF-none-match、IF-modified-since
  • 浏览器解析渲染页面

    • 解析HTML,构建dom树,词法分析和语法分析

    • 解析css,生成css规则树,从右往左解析

    • 合并DOM树和CSS规则树,生成render树

    • 布局render树,根据render节点的类型,确定元素大小和位置

    • 绘制render树,绘制页面像素信息

    • 浏览器将各层的信息发送给GUI,GUI将各层合成,展示在屏幕上

    • 细化流程:构件dom树、构建sytle Rules、样式计算、创建布局树、分层、绘制、分块和光栅化、合成和显示

      • 渲染是在渲染进程执⾏的,渲染进程分为渲染主线程、光栅线程、合成线程等

      • 从分块阶段开始,包括分块、光栅化、合成这三步是在⾮主渲染线程执⾏

      • 重排、重绘、合成:开发中尽量减少重排重绘

        • 重排:改变了 DOM 元素的⼏何位置属性,⽐如宽度、⾼度,那么就会触发重新布局(Layout 阶段),及之后的⼦阶段;重排需要更新完整的流⽔线,开销也⽐较⼤
        • 重绘:通过CSS 或 JS 改变了⾮ DOM 元素的⼏何位置属性,⽐如背景⾊、边框⾊等;那么会跳过布局、分层阶段,直接到绘制阶段,执⾏效率⽐重排⾼⼀些
        • 合成:CSS3 动画,⽐如transform,直接在合成线程上合成动画操作,效率⽐较⾼
  • 连接结束关闭TCP链接(四次挥手)

    • 第一次挥手是浏览器发完数据后,发送FIN请求断开连接,进入FIN_WAIT_1状态
    • 第二次挥手是服务器收到FIN报文,返回ACK报文段表示同意,进入FIN_WAIT_2状态
    • 第三次挥手是服务器发送FIN报文请求关闭连接,进入LAST_ACK状态
    • 第四次挥手是浏览器收到FIN报文段,向服务器发送ACK报文段,进入TIME_WAIT状态。服务器接收到ACK报文关闭连接,浏览器等待一段时间后,表示服务器已关闭连接,也关闭连接。

2、性能检测工具

原理:就是在合适的时机,打上合适的时间戳,或者暴露出事件。然后通过这些时间戳之间的差值,得出⼀个耗时时间。这个耗时时间就可以反映出我们⻚⾯的相关性能。工具如下:

1、performance

Performance 接口可以获取到当前页面中与性能相关的信息。它是 High Resolution Time API 的一部分,同时也融合了Performance Timeline API、Navigation Timing API、 User Timing API和 Resource Timing API

// 获取 performance 数据
var performance = {
    // memory 是⾮标准属性,只在 Chrome 有
    
    // 我有多少内存?
    memory: {
        usedJSHeapSize: 16100000, // JS 对象(包括V8引擎内部对象)占⽤的内存,⼀定⼩于 totalJSHeapSize
        totalJSHeapSize: 35100000, // 可使⽤的内存
        jsHeapSizeLimit: 793000000 // 内存⼤⼩限制
    },

    // 我从哪⾥来?
    navigation: {
        redirectCount: 0, // 如果有重定向的话,⻚⾯通过⼏次重定向跳转⽽来
        type: 0 // 0 即 TYPE_NAVIGATENEXT 正常进⼊的⻚⾯(⾮刷新、⾮重定向等)
                // 1 即 TYPE_RELOAD 通过window.location.reload() 刷新的⻚⾯
                // 2 即 TYPE_BACK_FORWARD 通过浏览器的前进后退按钮进⼊的⻚⾯(历史记录)
                // 255 即 TYPE_UNDEFINED ⾮以上⽅式进⼊的⻚⾯
    },

    // 核⼼时间相关
    timing: {
        // 在同⼀个浏览器上下⽂中,前⼀个⽹⻚(与当前⻚⾯不⼀定同域)unload 的时间戳,如果⽆前⼀个⽹⻚ unload ,则与 fetchStart 值相等
        navigationStart: 1441112691935,

        // 前⼀个⽹⻚(与当前⻚⾯同域)unload 的时间戳,如果⽆前⼀个⽹⻚ unload 或者前⼀个⽹⻚与当前⻚⾯不同域,则值为 0
        unloadEventStart: 0,

        // 和 unloadEventStart 相对应,返回前⼀个⽹⻚ unload 事件绑定的回调函数执⾏完毕的时间戳
        unloadEventEnd: 0,

        // 第⼀个 HTTP 重定向发⽣时的时间。有跳转且是同域名内的重定向才算,否则值为 0
        redirectStart: 0,

        // 最后⼀个 HTTP 重定向完成时的时间。有跳转且是同域名内部的重定向才算,否则值为 0
        redirectEnd: 0,

        // 浏览器准备好使⽤ HTTP 请求抓取⽂档的时间,这发⽣在检查本地缓存之前
        fetchStart: 1441112692155,

        // DNS 域名查询开始的时间,如果使⽤了本地缓存(即⽆ DNS 查询)或持久连接,则与 fetchStart 值相等
        domainLookupStart: 1441112692155,

        // DNS 域名查询完成的时间,如果使⽤了本地缓存(即⽆ DNS 查询)或持久连接,则与 fetchStart 值相等
        domainLookupEnd: 1441112692155,

        // HTTP(TCP) 开始建⽴连接的时间,如果是持久连接,则与 fetchStart 值相等
        // 注意如果在传输层发⽣了错误且重新建⽴连接,则这⾥显示的是新建⽴的连接开始的时间
        connectStart: 1441112692155,

        // HTTP(TCP) 完成建⽴连接的时间(完成握⼿),如果是持久连接,则与fetchStart 值相等
        // 注意如果在传输层发⽣了错误且重新建⽴连接,则这⾥显示的是新建⽴的连接完成的时间
        // 注意这⾥握⼿结束,包括安全连接建⽴完成、SOCKS 授权通过
        connectEnd: 1441112692155,

        // HTTPS 连接开始的时间,如果不是安全连接,则值为 0
        secureConnectionStart: 0,

        // HTTP 请求读取真实⽂档开始的时间(完成建⽴连接),包括从本地读取缓存
        // 连接错误重连时,这⾥显示的也是新建⽴连接的时间
        requestStart: 1441112692158,

        // HTTP 开始接收响应的时间(获取到第⼀个字节),包括从本地读取缓存
        responseStart: 1441112692686,

        // HTTP 响应全部接收完成的时间(获取到最后⼀个字节),包括从本地读取缓存
        responseEnd: 1441112692687,

        // 开始解析渲染 DOM 树的时间,此时 Document.readyState 变为 loading,并将抛出 readystatechange 相关事件
        domLoading: 1441112692690,

        // 完成解析 DOM 树的时间,Document.readyState 变为 interactive,并将抛出 readystatechange 相关事件
        // 注意只是 DOM 树解析完成,这时候并没有开始加载⽹⻚内的资源
        domInteractive: 1441112693093,

        // DOM 解析完成后,⽹⻚内资源加载开始的时间
        // 在 DOMContentLoaded 事件抛出前发⽣
        domContentLoadedEventStart: 1441112693093,

        // DOM 解析完成后,⽹⻚内资源加载完成的时间(如 JS 脚本加载执⾏完毕)
        domContentLoadedEventEnd: 1441112693101,

        // DOM 树解析完成,且资源也准备就绪的时间,Document.readyState 变为complete,并将抛出 readystatechange 相关事件
        domComplete: 1441112693214,

        // load 事件发送给⽂档,也即 load 回调函数开始执⾏的时间
        // 注意如果没有绑定 load 事件,值为 0
        loadEventStart: 1441112693214,

        // load 事件的回调函数执⾏完毕的时间
        loadEventEnd: 1441112693215

        // 按照字⺟排序
        // connectEnd: 1441112692155,
        // connectStart: 1441112692155,
        // domComplete: 1441112693214,
        // domContentLoadedEventEnd: 1441112693101,
        // domContentLoadedEventStart: 1441112693093,
        // domInteractive: 1441112693093,
        // domLoading: 1441112692690,
        // domainLookupEnd: 1441112692155,
        // domainLookupStart: 1441112692155,
        // fetchStart: 1441112692155,
        // loadEventEnd: 1441112693215,
        // loadEventStart: 1441112693214,
        // navigationStart: 1441112691935,
        // redirectEnd: 0,
        // redirectStart: 0,
        // requestStart: 1441112692158,
        // responseEnd: 1441112692687,
        // responseStart: 1441112692686,
        // secureConnectionStart: 0,
        // unloadEventEnd: 0,
        // unloadEventStart: 0
    }
}

使⽤ performance.timing 信息简单计算出⽹⻚性能数据

  • FP:responseStart - navigationStart
  • 重定向耗时:redirectEnd - redirectStart
  • DNS 查询耗时:domainLookupEnd - domainLookupStart
  • TCP 链接耗时:connectEnd - connectStart
  • HTTP 请求耗时:responseEnd - responseStart
  • 解析 dom 树耗时:domComplete - domInteractive
  • DOM ready 时间:domContentLoadedEventEnd - navigationStart
  • onload:loadEventEnd - navigationStart

2、performanceObserver

PerformanceObserver.observe() :指定监测的 entry types 的集合。 当 performance entry 被记录并且是指定的 entryTypes 之⼀的时候,性能观察者对象的回调函数会被调⽤。

var observer = new PerformanceObserver(callback);
//首个参数是性能观察者参数列表、第二个参数是观察者对象
var observer = new PerformanceObserver(function(list, obj) {
  var entries = list.getEntries();
  for (var i=0; i < entries.length; i++) {
    // Process "mark" and "frame" events
  }
});
//当记录一个指定类型的性能条目时,性能监测对象的回调函数将会被调用。
observer.observe({entryTypes: ["mark", "frame"]});

3、web-vitals

web-vitals: Google 于 2020 年 5 年 5 ⽇提出了新的使⽤者体验量化⽅式,推出 Web Vitals 是简化这个 学习的曲线,⼤家只要观注 Web Vitals 指标表现即可;

  • LCP 显示最⼤内容元素所需时间 (衡量⽹站初次载⼊速度)
  • FID ⾸次输⼊延迟时间 (衡量⽹站互动顺畅程度)
  • CLS 累计版⾯配置移转 (衡量⽹⻚元件视觉稳定性)

前端框架,⽬前只能统计'CLS' | 'FCP' | 'FID' | 'LCP' | 'TTFB' 。如果需要扩充的话,就可以使⽤上⾯的Performance 进⾏更改

import {getCLS, getFID, getLCP, getFCP, getTTFB} from 'web-vitals';

getCLS(console.log);
getFID(console.log);
getLCP(console.log);
getFCP(console.log);
getTTFB(console.log);

3、性能指标

1、白屏时间FP

输入URL开始,到页面开始有变化,只要有任意像素点的变化,就算是白屏时间完结

function getFP() {
    new PerformanceObserver((entryList, observer) => {
        let entries = entryList.getEntries();
        for (let i = 0; i < entries.length; i++) {
            if (entries[i].name === 'first-paint') {
              console.log('FP', entries[i].startTime);
            }
        }
    }).observe({entryTypes: ['paint']});
};

2、首次内容绘制时间FCP

指的是⻚⾯上绘制了第⼀个元素的时间

FP与FCP的最⼤的区别就在于:FP 指的是绘制像素,⽐如说⻚⾯的背景⾊是灰⾊的,那么在显示灰⾊背景时就记录下了 FP 指标。但是此时 DOM 内容还没开始绘制,可能需要⽂件下载、解析等过程,只有当 DOM 内容发⽣变化才会触发,⽐如说渲染出了⼀段⽂字,此时就会记录下 FCP 指标。因此说我们可以把这两个指标认为是和⽩屏时间相关的指标,所以肯定是最快越好。

function getFP() {
    new PerformanceObserver((entryList, observer) => {
        let entries = entryList.getEntries();
        for (let i = 0; i < entries.length; i++) {
            if (entries[i].name === 'first-contentful-paint') {
              console.log('FCP', entries[i].startTime);
            }
        }
    }).observe({entryTypes: ['paint']});
};

3、首页时间FIRSTPAGE

当onload事件触发的时候,也就是整个⾸⻚加载完成的时候

function getFirstPage() {
    console.log('FIRSTPAGE', (performance.timing.loadEventEnd - performance.timing.fetchStart));
};

4、最大内容绘制LCP

⽤于记录视窗内最⼤的元素绘制的时间,该时间会随着⻚⾯渲染变化⽽变化,因为⻚⾯中的最⼤元素在渲染过程中可能会发⽣改变,另外该指标会在⽤户第⼀次交互后停⽌记录。

function getLCP() {
    new PerformanceObserver((entryList, observer) => {
        let entries = entryList.getEntries();
        const lastEntry = entries[entries.length - 1];
        console.log('LCP', lastEntry.renderTime || lastEntry.loadTime);
    }).observe({entryTypes: ['largest-contentful-paint']});
}

5、首次可交互时间TTI

FCP指标后,首个长任务执行时间点,其后无长任务或2个get请求。

    1. 从 FCP 指标后开始计算
    1. 持续 5 秒内⽆⻓任务(执⾏时间超过 50 ms)且⽆两个以上正在进⾏中的 GET 请求
    1. 往前回溯⾄ 5 秒前的最后⼀个⻓任务结束的时间
function getTTI() {
    let time = performance.timing.domInteractive - performance.timing.fetchStart;
    console.log('TTI', time);
};

6、网络请求耗时TTFB

网络请求耗时(TTFB): responseStart - requestStart

7、首次输入延迟FID

从用户第一次与页面交互到浏览器实际能够开始处理事件的时间,在 FCP(首次内容绘制) 和 TTI (首次可交互时间)之间⽤户⾸次与⻚⾯交互时响应的延迟,eg:点击输入框后,因渲染等引起的延迟

function getFID() {
    new PerformanceObserver((entryList, observer) => {
          let firstInput = entryList.getEntries()[0];
          if (firstInput) {
              const FID = firstInput.processingStart - firstInput.startTime;
              console.log('FID', FID);
          }
    }).observe({type: 'first-input', buffered: true});
}

8、累计位置偏移CLS

⻚⾯渲染过程中突然插⼊⼀张巨⼤的图⽚或者说点击了某个按钮突然动态插⼊了⼀块内容等等相当影响⽤户体验的⽹站。这个指标就是为这种情况⽽⽣的,计算⽅式为:位移影响的⾯积 * 位移距离。如下图: 0.25 * 0.75 = 0.1875 。CLS 推荐值为低于 0.1

function getCLS() {
    try {
        let cumulativeLayoutShiftScore = 0;
        const observer = new PerformanceObserver((list) => {
            for (const entry of list.getEntries()) {
                // Only count layout shifts without recent user input.
                if (!entry.hadRecentInput) {
                    cumulativeLayoutShiftScore += entry.value;
                }
            }
        });
        observer.observe({type: 'layout-shift', buffered: true});
        document.addEventListener('visibilitychange', () => {
            if (document.visibilityState === 'hidden') {
                // Force any pending records to be dispatched.
                observer.takeRecords();
                observer.disconnect();
                console.log('CLS:', cumulativeLayoutShiftScore);
            }
        });
    } catch (e) {
        // Do nothing if the browser doesn't support this API.
    }
};

9、使用方法

  1. 问题:这几个指标怎么使用
  • vue:定义公用方法类,common.js,mounted阶段页面进行挂载,$nextTick()里对响应方法进行使用
  • react:hooks useEffect()中使用react useEffect(() => {}, []);
  • 公司内部使用打点系统:使用echars绘制,使用均值统计、百分位数统计、样本分布统计输出性能
  • 自己使用:使用谷歌lighthouse插件检查性能
  1. ⾕歌的标准,关注:LCP(最大内容绘制时间)、FID(首次输入延迟)、CIS(累计位移偏移)
GoodPoorPercentile
Largest Contentful Paint<=2500ms>4000ms75
First Input Delay<=100ms>300ms75
Cumulative Layout Shift<=0.1>0.2575
  • LCP 代表了页面的速度指标,虽然还存在其他的一些体现速度的指标。一是指标实时更新,数据更精确,二是代表着页面最大元素的渲染时间,通常来说页面中最大元素的快速载入能让用户感觉性能还挺好。
  • FID 代表了页面的交互体验指标,毕竟没有一个用户希望触发交互以后页面的反馈很迟缓,交互响应的快会让用户觉得网页挺流畅。
  • CLS 代表了页面的稳定指标,尤其在手机上这个指标更为重要。因为手机屏幕挺小,CLS 值一大的话会让用户觉得页面体验做的很差。

4、优化思路与方法

  1. 从浏览器输入url到页面各阶段做了什么,进行性能优化
  2. 根据前端性能指标进行优化
  3. 框架特有的性能优化点:小程序分包、vue路由按需加载等
  4. 优化方法:开发规范、技术架构设计、系统架构设计

性能优化.png

1、浏览器加载优化

  1. DNS预解析、预链接
<!-- 开启隐式预解析:默认情况,浏览器对a标签中与当前域名不在同一域的相关域名进行预获取且缓存结果,对于https失效 -->
<meta http-equiv="x-dns-prefetch-control" content="on">
<!-- 只解析域名,不进行资源下载 -->
<link rel="dns-prefetch" href="http://www.baidu.com" />
<!-- 将会做 DNS 解析,TLS 协商和 TCP 握手 -->
<link  rel="preconnect" href="//baidu.com">
  1. http请求阶段

    • 减少http请求合理利用时序:资源合并(雪碧图)、使用promise.all并发请求
    • 减少资源体积:减少cookie信息、图片格式优化、gzip静态资源压缩、webpack打包压缩
    • 合理利用缓存:cdn、http缓存(强缓存和协商缓存)、本地缓存(localStorage、sessionStorage)
  2. 浏览器渲染阶段:下载css并解析、下载js文件并解析会影响页面首屏渲染

    • 减少重排重绘,尽量使用css动画,或者添加will-change属性
    • script脚本放在body元素中⻚⾯内容的后⾯,避免JS阻碍html解析,减少⽩屏时间
    • css文件尽量放在head中,尽快下载和解析
    • 使用预解析和异步加载:prefetch、prerender、preload、async、defer
    • 服务器端渲染ssr
    • 资源按需引入:路由懒加载,组件库按需引入
<!-- 在浏览器空闲时下载资源 -->
<link rel="prefetch" href="https://css-tricks.com/a.png">
<!-- 浏览器会提前完成所有的资源加载,执行,渲染并保存在内存里 -->
<link rel="prerender" href="https://css-tricks.com">
<!-- 提前下载资源,影响资源加载顺序,后置下载资源前置下载 -->
<link rel="preload" href="https://fonts.gstatic.com/s/sofia/v8/bjl.woff2" as="font" crossorigin="anonymous">

<!-- ⽂件加载完成后,会执⾏此脚本,执⾏顺序⽆法保证,先加载完成的先执⾏ -->
<script src="./static/demo1.js" async></script>
<!-- 延迟执⾏脚本,解析完</html>后执⾏,执⾏顺序不变 -->
<script src="./static/defer-demo1.js" defer></script>

2、性能指标监测

  • FP 首次绘制(白屏时间)、LCP 最大内容渲染:路由懒加载、缓存、脚本异步加载
  • TTI首次可交互时间:performance.timing.domInteractive -performance.timing.fetchStart
  • FID首次输入延迟:路由懒加载、建少js计算逻辑
  • CIS累计位置偏移:css动画设置位移、图片设置具体宽高

3、优化方法

1、开发规范
  • css开发规范:雪碧图、图片格式优化、尽量减少重排和重绘,使用动画
  • js开发规范:promise并发、预解析和懒加载、开发细节(for循环缓存对象、尽量少使用闭包、递归的边界条件)
2、技术框架
  • 路由懒加载
  • 组件按需引入:babel插件转换
  • webpack打包优化配置:资源压缩、资源拆分部署至cdn(externals)
  • 小程序:分包加载、setData操作优化、限频接口调用优化等
3、架构优化
  • cdn预热
  • nginx缓存配置、gzip压缩开启
  • ssr及预渲染
  • 后端bigpipe引入:动态网页加载技术