LiuL0703 / blog

Home Page:https://liul0703.github.io

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

渲染性能分析(上)

LiuL0703 opened this issue · comments

如今大部分设备的刷新频率数60fps,什么意思呢?意思就是每秒屏幕刷新60次。举个例子:页面上出现动画或者渐变的效果,又或者用户滚动页面,那么浏览器渲染动画或页面的每一帧的频率也需要跟设备屏幕的刷新率保持一致。每帧的预算时间是16.66ms,这个时间段中浏览器要处理很多事情,所以最好的情况是在10ms内将所有工作做完,如果超出预算时间,那么帧率会下降,就会出现常见的卡顿现象,对用户体验带来负面影响

渲染过程

要想在预期时间内完成页面更新则主要有5个关键点需要关心,这些点决定了页面的渲染时间

frame-full-a9d1a7d7-0b9e-40b9-809a-40254258e7ad

  • JavaScript : 一般来说,JavaScript通常用来实现一些视觉变化的效果,比如来处理一些动画效果,或者对一个数据集排序,又或者修改页面的DOM元素等。当然除了JavaScript,还有一些常用的方法也可以实现类似的效果,比如:CSS Animation、Transitions、Web Aninations API。
  • Style : 这个过程主要是样式计算。是根据样式匹配选择器计算出哪些元素需要应用哪些属性规则的过程,知道规则后计算出每个元素的最终的样式。
  • Layout : 知道了各个元素对应的规则后,浏览器开始计算元素要占据的空间大小和在屏幕中的位置,页面布局模式意味着一个元素可能会影响其他元素,例如元素的宽一般会影响其子元素的宽度以及树中各处的节点。
  • Paint : 绘制是填充像素的过程。它涉及绘制出文本、颜色、图像、边框和阴影,基本上包括元素的每个可视部分,绘制一般在多个图层上完成
  • Composite : 由于页面的各部分内容可能被绘制在多个图层,因此需要按照正确的顺序绘制到屏幕上,特别是对于重叠在一起的元素来说,这个顺序是非常重要的。

这些部分都是很重要的,处理不当就会导致页面出现卡顿情况,所以为了做好这一点必须要确切的知道你的代码到底会影响渲染的哪个阶段

  1. JS/CSS ➔ Style ➔ Layout ➔ Paint ➔ Composite

    如果修改元素的"layout"属性,即改变元素的几何属性(例如宽高,或位置),那么浏览器必须检查其他所有元素并重新计算,受到影响的部分要重新绘制,最终进行合成。所以要重走整个过程

  2. JS/CSS ➔ Style ➔ Paint ➔ Composite

    如果修改的内容是属于“Paint”属性(比如:背景图片,文字颜色或阴影),这些不会影响到页面布局,所以浏览器会直接跳过Layout阶段。

  3. JS/CSS ➔ Style ➔ Composite

    如果修改的属性既不需要页面重新布局,也不需要重新绘制,那浏览器会跳过Paint和Layout阶段,这种情况开销最低,适合用在动画或滚动的情况

下面来详细说说每个阶段需要注意的问题

优化JavaScript的执行

JavaScript经常会触发一些页面视觉上改变,有时候是直接改变样式,有时候是更新页面数据,有时候是执行一些动画效果等等。JavaScript运行时间通常是影响性能的关键因素,所以接下来我们可以看一下如何去尽量减小这些因素的影响。

JavaScript的性能分析可能是一门艺术了,因为你所写的JavaScript代码和实际执行时的完全不同。如今的浏览器都采用的是JIT(Just In Time)编译器,同时会使用各种优化技巧以供最快速的执行,但是正因为如此却改变了代码本来的动态性。

如上所言,那么下面会给出一些建议来帮你更好的执行JavaScript代码

TL;DR

  • 使用requestAnimationFrame代替setTimeout或者setInterval来处理页面上的视觉变化
  • 考虑使用Web Worker来将需要在主线程长时间运行的JavaScript代码迁移
  • 将大的耗时任务拆分为多个task分为几帧完成
  • 使用Chrome DevTools中的Performance和JavaScript Profiler来观测对JavaScript的影响因素

使用requestAnimationFrame做动效

某些情况下在页面视觉上发生变化时,你可能想正好在每一帧开始时执行某些操作,那么requestAnimationFrame是唯一可以准确保证在每一帧执行前执行JS代码的方法

/**
 * 作为requestAnimationFrame的回调函数,会在每帧开始前执行
 */
function updateScreen(time){
    // ...
}
requestAnimationFrame(updateScreen);

一些框架或者示例可能使用setTimeout或者setInterval来做一些视觉上的变动比如动画,但是问题是无法确定这些回调函数的执行时间点,有可能恰巧是在每帧的结尾,那就可能导致帧丢失,从而导致页面卡顿,这完全不是我们想要的。
Snipaste_2019-10-21_10-24-45-5c703a8e-78ee-423c-8bd1-b5a312855092

实际上jQuery以前也用setTimeout来执行动画,在后来的版本中改用requestAnimationFrame了,如果你还在使用旧版本的话可以检查一下,有必要可以考虑升级。(应该人很少了吧)

减少复杂度的或者使用Web Worker

JavaScript运行在浏览器的主线程上,与此同时主线程还要执行样式计算,布局,绘制等等。如果JavaScript代码长时间执行则会阻塞这些任务,就可能出现帧丢失的情况。

所以需要考量JavaScript代码的执行时间点和执行时长。举个例子:如果在执行滚动操作,那么理想情况下应该保持JS代码的执行时间保持在3~4ms内,如果超过这个时间,就要考虑采取优化手段了,如果是在空闲时间段那就可以放宽时间限制了。

很多情况下如果不需要访问DOM,就可以把一些纯计算的工作交给Web Worker去执行,对于数据的处理或者搜索排序等等操作都非常适合在这里处理

const dataSortWorker = new Worker('sort-worker.js');
dataSortWorker.postMessage(dataToSort);

// 主线程则可以做其他事情
dataSortWorker.addEventListener('message',(e)=>{
    const sortedData = e.data;
    // ... 
})

也不是所有情况都适合:Web Worker无法访问DOM。在必须在主线程执行的工作,可以考虑采用批处理方法,什么意思呢?就是把较大的任务分割成多个task,每个task不超过几毫秒,并且放在requestAnimationFrame中,让其在每帧的开始去执行。

const taskList = breakBigTaskIntoMicroTasks(monsterTaskList);
requestAnimationFrame(processTaskList);

function processTaskList(taskStartTiem){
    let taskFinishTime;
    do {
        // 假设下一个task已经推到栈里了
        const nextTask = taskList.pop();
        // 执行下一个task
        processTask(nextTask);

        // 
        taskFinishTime = window.performance.now();
    } while(taskFinishTime - taskStartTime < 3);
    
    if(taskList.length > 0){
        requestAinmationFrame(processTaskList);
    }
}

这种处理方法从UI方面考虑,可以加一个进度标识图标以便让用户知晓任务正在执行。不管怎样它都可以保证程序的主线程是空闲状态,因此不会影响用户交互行为。

知晓JavaScript的帧的副作用

在评估一个库或者一个框架亦或自己的代码时,逐帧分析JS代码的执行消耗的时间是很必要的。特别是在动画或者一些过渡效果方面时尤为重要。

Chrome DevTools提供的Performance功能是查看JS每帧执行消耗时间的非常好的工具。

Snipaste_2019-10-21_11-51-28-18b91852-90a7-4f9b-9c7c-75a55a7cdc7b

通过这个工具提供的信息分析后就可以找出影响性能的原因,如我们之前所提到的,如果在主线程中长时间执行的JS代码是非必要的就可以把它移到Web Worker中来让主线程执行其他任务。【Performance的使用方法

对于Style Calculation阶段的优化

避免嵌套过深和复杂的样式计算

通过添加和删除元素,更改属性,或者通过动画来改变DOM结构等都会导致浏览器重新计算元素样式,很多情况下都会重新对整个页面或其中部分布局(layout)(或者回流[reflow]),这个过程也叫样式计算。

💡:关于repaint和reflow的区别:repaint 指元素只发生了外观的变化,但是不影响布局,页面只需要做重绘即可。会触发repaint的常见CSS属性比如:outline, visibility, background, 或 color。relfow 则是发生了几何上的变化需要对元素进行重新计算和布局。例如元素的width,height等。

样式计算的第一步就是创建一个与之对应的选择器集合,实际上就是让浏览器确定哪些类哪些伪类选择器和ID该应用于哪个元素,第二步是从匹配的选择器中获取所有样式,并计算最终样式。在Blink(Chrome和Opera的渲染引擎)中这个过程还是相当消耗性能的。

这个过程中渲染引擎大概有50%的时间在匹配选择器,剩下的一半时间在计算最终样式。

TL;DR

  • 降低选择器的复杂度,即避免嵌套太深
  • 减少在样式中的计算

降低选择器的复杂度

我们知道浏览器在解析匹配CSS规则时是从右向左查找匹配的,嵌套越深选择匹配的负担越重,最好不要超过三层。同时浏览器在解析生成页面时分别解析构建DOM Tree 和CSSOM,在DOM树构建完成CSSOM未构建完成时,是不会直接把html放出来的,所以CSS不能太大,否则会有一段白屏时间,所以把字体或者图片转成base64放在CSS里是不太推荐的做法。

有些CSS优化建议说要按照如下优先级书写:

  1. 位置属性【position,top,left,right,z-index...】
  2. 大小【width,height...】
  3. 字体相关【font,text-align...】
  4. 背景【background,border...】
  5. 其他【animation,transition...】

其实这些顺序对浏览器来说是一样的,因为浏览器在解析构建CSS规则时并不立马进行渲染,而是把这些属性值合并、归类、并将最终计算出的属性值放到computedStyle里,之后交由Layout阶段去计算实际显示值,Paint阶段才会去绘制。所以顺序并不会带来性能上的影响,对浏览器而言都是一样的。

关于样式中的计算比较典型一个例子的可能就是对元素使用rgba函数(或者使用calc),当CSS解析时就需要先去执行rgba函数计算颜色,所以直接写成16位的色值更好一些。

参考链接: Rendering Performance