浅谈渲染性能优化及监控

963 阅读9分钟

本文会提到如何确认白屏及首屏时间。以及影响这些时间的因素有哪些。

先说结论

简单优化方面

  • css放在头,js放在尾(前面),js想放在头要加defer。 确认时间方面
  • 简单应用计算白屏时间就行。
  • 复杂应用计算首屏时间才有意义,其中spa要计算lcp,不是spa可以计算DOMContentLoaded触发时的时间(即timing.domContentLoadedEventStart - timing.navigationStart)

其他:后续在文中的资料来源中可以看到其他优化手段想了解自行去深入阅读,包括:

  • 在加载阶段,核心的优化原则是:优化关键资源的加载速度,减少关键资源的个数,降低关键资源的 RTT 次数。
  • 在交互阶段,核心的优化原则是:尽量减少一帧的生成时间。可以通过减少单次 JavaScript 的执行时间、避免强制同步布局、避免布局抖动、尽量采用 CSS 的合成动画、避免频繁的垃圾回收等方式来减少一帧生成的时长。

渲染

资料来源:从web浏览器的渲染到性能优化

js阻塞渲染

js跟dom渲染的关系:

20170803161021015.png

先讲一下规范里经典script的处理方法,总共三种情况:

  • 不使用async和defer,根据所在位置阻塞解析,被下载紧接着执行直到完成。
  • 使用async,不阻塞解析并行下载脚本,当下载完成后阻塞解析立即执行,在解析完成前后都可能被执行。
  • 使用defer,并行下载,并在页面完成解析时进行执行,不会阻塞解析

注意:当CSS样式文件没有下载完成时,浏览器解析HTML遇到了内联JS代码,此时!!!根据浏览器的安全解析策略,浏览器暂停JS脚本执行,暂停HTML解析。直到CSS文件下载完成,完成CSSOM树构建,重新恢复原来的解析

html在传输过程中html parse就开始(不用等到html文件下载完)

简单来说大致渲染流程是,加载html文件构成dom树,加载css文件构成cssom树,两者结合构成render-tree

然后之后是layout(计算每个元素所占据的位置坐标,将rem,vw,em等相对测量值转换为屏幕上的绝对像素)

css阻塞渲染

cssom形成前,浏览器不会渲染任何已处理内容。所以我们对css资源可以进行以下几个优化:

  • 媒体查询
  • preload
  • 动态添加link
  • 少用通配符,高级选择器

preload(preload是resoure hint规范中定义的一个功能,顾名思义预加载,将rel改为preload后,相当于加了一个标志位,浏览器解析的时候会提前建立连接或加载资源,做到尽早并行下载):

rel="preload" href="index_print.css" as="style">

这里的preload应该是不用加的:理由是浏览器工作原理与实践中提到:

由于渲染引擎有一个预解析的线程,在接收到 HTML 数据之后,预解析线程会快速扫描 HTML 数据中的关键资源,一旦扫描到了,会立马发起请求,你可以认为 JavaScript 和 CSS 是同时发起请求的

根据开发者工具也可以观察到css确实会被自动预解析

font阻塞渲染

字体加载也会阻塞渲染,只有当字体超过一段时间仍未加载成功时,浏览器才会降级使用系统字体。每个浏览器都规定了自己的超时时间。这也带来了FOIT(Flash Of Invisible Text)问题。内容无法尽快地被展示,导致空白。

性能监控

要计算白屏、首屏时间,首先先来看看浏览器提供给我们的performance api:

performance

  • memory:显示此刻内存占用情况,是一个动态值
    • usedJSHeapSize:JS对象占用的内存数
    • jsHeapSizeLimit:可使用的内存
    • totalJSHeapSize:内存大小限制
  • navigation:显示页面的来源信息
    • redirectCount:表示如果有重定向的话,页面通过几次重定向跳转而来,默认为0
    • type:表示页面打开的方式。0-正常进入;1-通过window.reload()刷新的页面;2-通过浏览器的前进后退按钮进入的页面;255-非以上方式进入的页面。
  • onresourcetimingbufferfull:在resourcetimingbufferfull事件触发时会被调用的一个event handler。它的值是一个手动设置的回调函数,这个回调函数会在浏览器的资源时间性能缓冲区满时执行。
  • timeOrigin:一系列时间点的基准点,精确到万分之一毫秒。
  • timing:一系列关键时间点,包含网络、解析等一系列的时间数据。
    • navigationStart:同一个浏览器上一个页面卸载(unload)结束时的时间戳。如果没有上一个页面,这个值会和fetchStart相同
    • unloadEventStart: 上一个页面unload事件抛出时的时间戳。如果没有上一个页面,这个值会返回0。
    • unloadEventEnd: 和 unloadEventStart 相对应,unload事件处理完成时的时间戳。如果没有上一个页面,这个值会返回0。
    • redirectStart: 第一个HTTP重定向开始时的时间戳。如果没有重定向,或者重定向中的一个不同源,这个值会返回0
    • redirectEnd: 最后一个HTTP重定向完成时(也就是说是HTTP响应的最后一个比特直接被收到的时间)的时间戳。如果没有重定向,或者重定向中的一个不同源,这个值会返回0
    • fetchStart: 浏览器准备好使用HTTP请求来获取(fetch)文档的时间戳。这个时间点会在检查任何应用缓存之前。
    • domainLookupStart: DNS 域名查询开始的UNIX时间戳。如果使用了持续连接(persistent connection),或者这个信息存储到了缓存或者本地资源上,这个值将和fetchStart一致。
    • domainLookupEnd: DNS 域名查询完成的时间。如果使用了本地缓存(即无 DNS 查询)或持久连接,则与 fetchStart 值相等
    • connectStart: HTTP(TCP) 域名查询结束的时间戳。如果使用了持续连接(persistent connection),或者这个信息存储到了缓存或者本地资源上,这个值将和 fetchStart一致。
    • connectEnd: HTTP(TCP) 返回浏览器与服务器之间的连接建立时的时间戳。如果建立的是持久连接,则返回值等同于fetchStart属性的值。连接建立指的是所有握手和认证过程全部结束。
    • secureConnectionStart: HTTPS 返回浏览器与服务器开始安全链接的握手时的时间戳。如果当前网页不要求安全连接,则返回0。
    • requestStart: 返回浏览器向服务器发出HTTP请求时(或开始读取本地缓存时)的时间戳。
    • responseStart: 返回浏览器从服务器收到(或从本地缓存读取)第一个字节时的时间戳。如果传输层在开始请求之后失败并且连接被重开,该属性将会被数制成新的请求的相对应的发起时间。
    • responseEnd: 返回浏览器从服务器收到(或从本地缓存读取,或从本地资源读取)最后一个字节时。(如果在此之前HTTP连接已经关闭,则返回关闭时)的时间戳。
    • domLoading: 当前网页DOM结构开始解析时(即Document.readyState属性变为“loading”、相应的 readystatechange事件触发时)的时间戳。
    • domInteractive: 当前网页DOM结构结束解析、开始加载内嵌资源时(即Document.readyState属性变为“interactive”、相应的readystatechange事件触发时)的时间戳。
    • domContentLoadedEventStart: 当解析器发送DOMContentLoaded 事件,即所有需要被执行的脚本已经被解析时的时间戳。
    • domContentLoadedEventEnd: 当所有需要立即执行的脚本已经被执行(不论执行顺序)时的时间戳。
    • domComplete: 当前文档解析完成,即Document.readyState 变为 'complete’且相对应的readystatechange 被触发时的时间戳
    • loadEventStart: load事件被发送时的时间戳。如果这个事件还未被发送,它的值将会是0。
    • loadEventEnd: 当load事件结束,即加载事件完成时的时间戳。如果这个事件还未被发送,或者尚未完成,它的值将会是0

重定向耗时:redirectEnd - redirectStart

DNS查询耗时:domainLookupEnd - domainLookupStart

TCP链接耗时:connectEnd - connectStart

HTTP请求耗时:responseEnd - responseStart

解析dom树耗时:domComplete - domInteractive

白屏时间:responseStart - navigationStart

DOM ready时间:domContentLoadedEventEnd - navigationStart

onload时间:loadEventEnd - navigationStart

对于计算首屏时间还是白屏时间,如果页面简单,那么白屏时间即可。如果复杂且两屏时间间隔比较远,那么计算首屏时间会更有用

目前白屏常见的优化方案有:

  • SSR
  • 预渲染
  • 骨架屏

首屏时间可以用DOMContentLoaded触发的时间计算(timing.domContentLoadedEventStart - timing.navigationStart),但是对于spa不适用,因为spa主要通过js来动态的添加页面。

对于SPA,原先的做法是计算FMP(First Meaningful Paint,阅读参考:统计页面首屏时间,很多人第一步就错了)

  • 1、监听元素加载,主要是为了计算Dom的分数
  • 2、计算分数的曲率,计算出最终的FMP值

由于fmp的计算可能不同浏览器会有出入,所以被废弃:

Lighthouse有提供FMP(在 Lighthouse 6.0 中已经废弃了,这里有解释原因: web.dev,用Largest Contentful Paint代替),但浏览器没有提供

所以我们现在用LCP来进行测量

在 JavaScript 中测量 LCP

要在 JavaScript 中测量 LCP,您可以使用最大内容绘制 API 。以下示例说明了如何创建一个PerformanceObserver来侦听largest-contentful-paint条目并记录在控制台中。

new PerformanceObserver((entryList) => {
    for (const entry of entryList.getEntries()) {
        console.log('LCP candidate:', entry.startTime, entry);
    }
}).observe({type: 'largest-contentful-paint', buffered: true});

每条记录在案的largest-contentful-paint条目代表当前的 LCP 候选对象。通常情况下,最近条目发射的startTime值就是 LCP 值,但情况并非总是如此。并不是所有的largest-contentful-paint条目都能够用来测量 LCP。

Lighthouse的lcp指标和 API 之间有差异,开发者不必记住所有这些细微差异,而是可以使用web-vitals JavaScript 库来测量 LCP,库会自行处理这些差异(在可能的情况下):

import {getLCP} from 'web-vitals'; 
// 当 LCP 可用时立即进行测量和记录。 
getLCP(console.log);