渲染性能分析上

4次阅读

共计 4451 个字符,预计需要花费 12 分钟才能阅读完成。

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

渲染过程

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

  • JavaScript : 一般来说,JavaScript 通常用来实现一些视觉变化的效果,比如来处理一些动画效果,或者对一个数据集排序,又或者修改页面的 DOM 元素等。当然除了 JavaScript,还有一些常用的方法也可以实现类似的效果,比如:CSS Animation、Transitions、Web Aninations API。
  • Style : 这个过程主要是样式计算。是根据样式匹配选择器计算出哪些元素需要应用哪些属性规则的过程,知道规则后计算出每个元素的最终的样式。
  • Layout : 知道了各个元素对应的规则后,浏览器开始计算元素要占据的空间大小和在屏幕中的位置,页面布局模式意味着一个元素可能会影响其他元素,例如 <body> 元素的宽一般会影响其子元素的宽度以及树中各处的节点。
  • 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 来做一些视觉上的变动比如动画,但是问题是无法确定这些回调函数的执行时间点,有可能恰巧是在每帧的结尾,那就可能导致帧丢失,从而导致页面卡顿,这完全不是我们想要的。

实际上 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 每帧执行消耗时间的非常好的工具。

通过这个工具提供的信息分析后就可以找出影响性能的原因,如我们之前所提到的,如果在主线程中长时间执行的 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

原文:渲染性能分析 (上)
下篇:渲染性能分析(下)

正文完
 0

渲染性能分析上

4次阅读

共计 4451 个字符,预计需要花费 12 分钟才能阅读完成。

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

渲染过程

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

  • JavaScript : 一般来说,JavaScript 通常用来实现一些视觉变化的效果,比如来处理一些动画效果,或者对一个数据集排序,又或者修改页面的 DOM 元素等。当然除了 JavaScript,还有一些常用的方法也可以实现类似的效果,比如:CSS Animation、Transitions、Web Aninations API。
  • Style : 这个过程主要是样式计算。是根据样式匹配选择器计算出哪些元素需要应用哪些属性规则的过程,知道规则后计算出每个元素的最终的样式。
  • Layout : 知道了各个元素对应的规则后,浏览器开始计算元素要占据的空间大小和在屏幕中的位置,页面布局模式意味着一个元素可能会影响其他元素,例如 <body> 元素的宽一般会影响其子元素的宽度以及树中各处的节点。
  • 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 来做一些视觉上的变动比如动画,但是问题是无法确定这些回调函数的执行时间点,有可能恰巧是在每帧的结尾,那就可能导致帧丢失,从而导致页面卡顿,这完全不是我们想要的。

实际上 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 每帧执行消耗时间的非常好的工具。

通过这个工具提供的信息分析后就可以找出影响性能的原因,如我们之前所提到的,如果在主线程中长时间执行的 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

原文:渲染性能分析 (上)
下篇:渲染性能分析(下)

正文完
 0