前端性能优化 之 渲染性能优化
前端性能优化,分为两个部分:
- 加载性能优化
- 渲染性能优化
本篇随笔介绍渲染性能优化。
渲染性能优化
浏览器渲染过程
- 1、解析HTML,生成DOM树。
- 2、解析CSS,生成CSSDOM规则树。
- 3、解析JS,操作DOM树和CSSDOM规则树。
- 4、将DOM树和CSSDOM规则树合并在一起,生成渲染树。
- 5、遍历渲染树开始布局,计算每个节点的位置大小信息。
- 6、浏览器将所有图层的数据发送给GPU,GPU将图层合成并且显示在屏幕上。
重排(reflow)与重绘(repaint)
重排
- 当改变 DOM 元素位置或大小时,会导致浏览器重新生成渲染树,这个过程叫重排。
重绘
- 当重新生成渲染树后,就要将渲染树每个节点绘制到屏幕,这个过程叫重绘。
重排必定引起重绘。
重绘不一定会引起重排,例如改变字体颜色,只会导致重绘。
重排和重绘这两个操作代价非常大。因为浏览器有两个线程,一个是执行JS的线程,一个是UI渲染的线程。二者互斥,UI渲染时,不执行js;执行js时,不进行UI渲染。
因此重排和重绘会阻塞主线程。
渲染性能优化方式
1)资源加载优先级控制
- preload/prefetch
- preload提前加载
- 顾名思义就是一种预加载的方式,它通过声明向浏览器声明一个需要提前加载的资源,当资源真正被使用的时候立即执行,就无需等待网络的消耗。
- 它可以通过 Link 标签进行创建:
-
<link rel="preload" href="/path/to/style.css" as="style">
- 当浏览器解析到这行代码就会去加载 href 中对应的资源但不执行,待到真正使用到的时候再执行。
- 使用 as 来指定将要预加载的内容的类型,将使得浏览器能够:
- 更精确地优化资源加载优先级。
- 匹配未来的加载需求,在适当的情况下,重复利用同一资源。
- 为资源应用正确的内容安全策略。
- 为资源设置正确的 Accept 请求头。
-
- 使用 HTTP 响应头的 Link 字段创建:
-
Link: <https://example.com/other/styles.css>; rel=preload; as=style
-
- 使用 preload 需要注意的点:
- 不要滥用 preload
- 如果不确定资源是否使用,则不要无意义的使用 preload,尤其是在移动端,会浪费用户带宽。
- preload 会使资源优先加载,但不一定会提升优先级
- 使用 preload 预加载跨域资源时,需要设置 crossorigin 属性:
-
<link rel="preload" href="https://mdn.github.io/html-examples/link-rel-preload/fonts/fonts/cicle_fina-webfont.woff2 " as="font" crossorigin="anonymous">
- 如果你需要获取的是 font 文件,那么即使是非跨域的情况下,也需要设置 crossorigin。
-
- 不要滥用 preload
- prefetch 预判加载
- prefetch 跟 preload 不同,它的作用是告诉浏览器未来可能会使用到的某个资源,浏览器就会在闲时去加载对应的资源。它的用法与preload一致:
-
<!-- link 模式 --> <link rel="prefetch" href="/path/to/style.css" as="style"> <!-- HTTP 响应头模式 --> Link: <https://example.com/other/styles.css>; rel=prefetch; as=style
-
- prefetch 跟 preload 不同,它的作用是告诉浏览器未来可能会使用到的某个资源,浏览器就会在闲时去加载对应的资源。它的用法与preload一致:
- 区别:
- preload 是一种肯定,确认会加载指定资源,在页面加载的生命周期的早期阶段就开始获取,不区分下一屏。页面一定会使用 preload 指定的资源(不使用将会报警告)。
- prefetch 是一种期望,预测会加载指定的资源,以备下一个导航或者下一屏页面使用,但对当前的页面并没有什么帮助。如果 prefetch 使用不得当,还会造成资源重复加载的问题。页面不一定会使用 prefetch 指定的资源。
- 细节:
- 当一个资源被 preload 或者 prefetch 获取后,它将被放在内存缓存中等待被使用,如果资源存在有效的缓存标志(如 cache-control 或 max-age),它将被存储在 HTTP 缓存中可以被不同页面所使用。
- 正确使用 preload/prefetch 不会造成二次下载,也就说:当页面上使用到这个资源时候 preload 资源还没下载完,这时候不会造成二次下载,会等待第一次下载并执行脚本。
- 对于 preload 来说,一旦页面关闭了,它就会立即停止 preload 获取资源,而对于 prefetch 资源,即使页面关闭,prefetch 发起的请求仍会进行不会中断。
- preload 是告诉浏览器页面必定需要的资源,浏览器一定会加载这些资源,而 prefetch 是告诉浏览器页面可能需要的资源,浏览器不一定会加载这些资源。没有用到的 preload 资源在 Chrome 的 console 里会在 onload 事件 3s 后发生警告。
- preload提前加载
2)减少重排重绘
-
减少页面DOM操作
- 在浏览器的渲染过程中,DOM 的操作是最消耗性能的。因为大部分的JavaScript DOM操作会导致一系列的重排重绘。
- 一般在我们的开发过程中,当DOM元素的操作不可避免时, 我们可以通过以下方式来尽量减少重绘与重排,重点是减少重排。主要的思路是, 将多次的DOM 操作合并为一次,或者使需要被操作的元素脱离文档流以减少浏览器重绘与重排的次数:
- 1、批量操作CSS样式
-
// javascript var el = document.querySelector('.el'); el.style.borderLeft = '1px'; el.style.borderRight = '2px'; el.style.padding = '5px';
- 这个例子在最糟糕的状况下,会触发浏览器三次重排。改成使用cssText属性实现:
-
var el = document.querySelector('.el'); el.style.cssText = 'border-left: 1px; border-right: 2px; padding: 5px';
- 沿着这个思路,你可以直接改个类名。没错,还有一种减小重排的方法就是切换类名,而不是使用内联样式的cssText方法。使用切换类名就变成了这样:
-
// css .active { padding: 5px; border-left: 1px; border-right: 2px; } // javascript var el = document.querySelector('.el'); el.className = 'active';
-
- 2、批量操作DOM元素
- 批量修改DOM元素的核心思想是:
- 让该元素脱离文档流
- 对其进行多重改变
- 将元素带回文档中
- 这个过程引起俩次重排,第一步和第三步,若是没有这两步,能够想象一下,第二步每次对DOM的增删都会引起一次重排。
- 知道批量修改DOM的核心思想后,再了解三种可使元素能够脱离文档流的方法。
- 隐藏元素,进行修改后,而后再显示该元素
- 使用文档片断建立一个子树,而后再拷贝到文档中
- 将原始元素拷贝到一个独立的节点中,操做这个节点,而后覆盖原始元素
- 代码示例:
-
// html <ul id="mylist"> <li><a href="https://www.mi.com">xiaomi</a></li> <li><a href="https://www.miui.com">miui</a></li> </ul> // javascript 如今须要添加带有以下信息的li节点 let data = [ { name: 'tom', url: 'https://www.baidu.com', }, { name: 'ann', url: 'https://www.techFE.com' } ]
- 忽视全部的重排因素,你们确定会这么写:
-
// javascript function appendNode($node, data) { var a, li; for(let i = 0, max = data.length; i < max; i++) { a = document.createElement('a'); li = document.createElement('li'); a.href = data[i].url; a.appendChild(document.createTextNode(data[i].name)); li.appendChild(a); $node.appendChild(li); } } let ul = document.querySelector('#mylist'); appendNode(ul, data);
- 使用这种方法,在没有任何优化的状况下,每次插入新的节点都会形成一次重排。考虑这个场景,若是咱们添加的节点数量众多,并且布局复杂,样式复杂,那么能想到的是你的页面必定很是卡顿。
- 1)隐藏元素,进行修改后,而后再显示该元素
-
let ul = document.querySelector('#mylist'); ul.style.display = 'none'; appendNode(ul, data); ul.style.display = 'block';
- 这种方法形成俩次重排,分别是控制元素的显示与隐藏。对于复杂的,数量巨大的节点段落能够考虑这种方法。为啥使用display属性呢,因为display为none的时候,元素就不在文档流了。
-
- 2)使用文档片断建立一个子树,而后再拷贝到文档中
-
let fragment = document.createDocumentFragment(); appendNode(fragment, data); ul.appendChild(fragment);
- 推荐此方法。文档片断是一个轻量级的document对象,它设计的目的就是用于更新,移动节点之类的任务,并且文档片断还有一个好处就是,当向一个节点添加文档片断时,添加的是文档片断的子节点群,自身不会被添加进去。不一样于第一种方法,这个方法并不会使元素短暂消失形成逻辑问题。上面这个例子,只在添加文档片断的时候涉及到了一次重排。
-
- 3)将原始元素拷贝到一个独立的节点中,操做这个节点,而后覆盖原始元素
-
let old = document.querySelector('#mylist'); let clone = old.cloneNode(true); appendNode(clone, data); old.parentNode.replaceChild(clone, old);
- 这种方法也是只有一次重排。总的来讲,使用文档片断,能够操做更少的DOM(对比使用克隆节点),最小化重排重绘次数。
-
-
- 批量修改DOM元素的核心思想是:
- 3、缓存布局信息
- 当访问诸如offsetLeft,clientTop这种属性时,会冲破浏览器自有的优化————经过队列化修改和批量运行的方法,减小重排/重绘版次。因此咱们应该尽可能减小对布局信息的查询次数,查询时,将其赋值给局部变量,使用局部变量参与计算。
- 性能糟糕的代码:
-
div.style.left = 1 + div.offsetLeft + 'px'; div.style.top = 1 + div.offsetTop + 'px';
-
- 这样形成的问题就是,每次都会访问div的offsetLeft,形成浏览器强制刷新渲染队列以获取最新的offsetLeft值。更好的办法就是,将这个值保存下来,避免重复取值。
- 性能优秀的代码:
-
current = div.offsetLeft; div.style.left = 1 + ++current + 'px'; div.style.top = 1 + ++current + 'px';
-
- 1、批量操作CSS样式
3)Web Worker
- 用于那些处理纯数据,或者与浏览器 UI 无关的长时间运行脚本;
页面加载指标
API指标
-
window.performence.timing
-
fetchStart
- 浏览器准备好使用 HTTP 请求抓取文档的时间,这发生在检查本地缓存之前。
-
domainLookupStart/domainLookupEnd
- DNS 域名查询开始/结束的时间,如果使用了本地缓存(即无 DNS 查询)或持久连接,则与 fetchStart 值相等;
-
connectStart
- HTTP(TCP)开始/重新 建立连接的时间,如果是持久连接,则与 fetchStart 值相等。
-
requestStart
- HTTP 请求读取真实文档开始的时间(完成建立连接),包括从本地读取缓存。
-
responseStart
- HTTP 开始接收响应的时间(获取到第一个字节),包括从本地读取缓存。
-
responseEnd
- HTTP 响应全部接收完成的时间(获取到最后一个字节),包括从本地读取缓存。
-
domLoading
- 开始解析渲染 DOM 树的时间,此时 Document.readyState 变为 loading,并将抛出 readystatechange 相关事件。
-
domInteractive
- 完成解析 DOM 树的时间,Document.readyState 变为 interactive,并将抛出 readystatechange 相关事件,注意只是DOM 树解析完成,这时候并没有开始加载网页内的资源。
-
domContentLoadedEventStart
- DOM 解析完成后,网页内资源加载开始的时间,在 DOMContentLoaded 事件抛出前发生。
-
domContentLoadedEventEnd
- DOM 解析完成后,网页内资源加载完成的时间(如 JS 脚本加载执行完毕)。
-
domComplete
- DOM 树解析完成,且资源也准备就绪的时间,Document.readyState 变为 complete,并将抛出 readystatechange 相关事件。
-
loadEventStart
- load 事件发送给文档,也即 load 回调函数开始执行的时间。
-
loadEventEnd
- load 事件的回调函数执行完毕的时间。
页面指标
白屏时间
-
window.performence.timing.domLoading - window.performence.timing.fetchStart
-
指浏览器发起请求到开始显示第一个页面元素的时间。
-
现代浏览器不会等待CSS树(所有CSS文件下载和解析完成)和DOM树(整个body标签解析完成)构建完成才开始绘制,而是马上开始显示中间结果。所以经常在低网速的环境中,观察到页面由上至下缓慢显示完,或者先显示文本内容后再重绘成带有格式的页面内容。
首屏时间
- 首屏时间(FirstScreen Time),是指用户看到第一屏,即整个网页顶部大小为当前窗口的区域,显示完整的时间。
- 常用的方法有,页面标签标记法、图像相似度比较法和首屏高度内图片加载法。
可交互时间
- window.performence.timing.domContentLoadedEventEnd - window.performence.timing.fetchStart
- 用户可以进行正常的点击、输入等操作,默认可以统计DOMContentLoaded事件发生的时间。
整页时间
- window.performence.timing.loadEventEnd - window.performence.timing.connectStart
- 整页时间(Page Load Time),页面所有资源都加载完成并呈现出来所花的时间,这个就是load事件发生的时间。
参考文章:
http://www.javashuo.com/article/p-pvzdgdrl-cs.html
https://www.pudn.com/news/6281a211ebb030486d5c3479.html#_135