渲染性能

Google Web Fundamentals Rendering Performance 的一篇笔记

https://developers.google.com/web/fundamentals/performance/rendering

大多数的设备的屏幕刷新率为 60fps ,那么每个帧的预算时间为 16.66ms,但是实际上浏览器还有整理工作要做,所有的工作应在 10ms 内完成。

像素管道

JavaScript 执行优化

  • 对于动画效果的实现,避免使用 setTimeoutsetInterval,请使用 requestAnimationFrame

requestAnimationFrame 能保证任务刚好在帧的开头开始执行,如果采用 setTimeout 或者 setInterval 的话,回调将在帧中的某个时点运行,可能刚好在末尾,而这可能经常会丢失帧,导致卡顿。

  • 将耗时长的纯计算工作,那些不需要访问 dom 的任务放到 Web Worker 执行,比如排序
// hljs-worker.js
import { highlightAuto } from 'highlight.js'
self.onmessage = evt => {
if (Array.isArray(evt.data)) {
const result = evt.data.map(html => highlightAuto(html).value)
postMessage(result)
return
}
const result = highlightAuto(evt.data)
postMessage(result.value)
}
/**********/
// index.js
import Worker from '../hljs-worker.js'
const codeBlocks = Array.from(document.querySelectorAll('code'))
if (!codeBlocks.length) return
const worker = new Worker()
worker.onmessage = evt => {
codeBlocks.forEach((codeBlock, index) => codeBlock.innerHTML = evt.data[index])
}
const htmls = codeBlocks.map(codeBlock => codeBlock.innerHTML)
worker.postMessage(htmls)
  • 如果有一个需要在主线程执行的大任务(需要访问 dom),那么可以将大任务拆分成很多个微任务,每个微任务所占时间不超过几毫秒,并且在每帧的 requestAnimationFrame 处理程序内运行。
const taskList = splitTask(megaTask)
const doTask = () => {
const taskStartTime = window.performance.now()
let taskFinishTime
  do {
const task = taskList.pop()
task()
    taskFinishTime = window.performance.now()
} while (taskFinishTime - taskStartTime <= 3)
  if (taskList.length > 0)
requestAnimationFrame(doTask)
}
requestAnimationFrame(doTask)

样式计算优化

计算样式分为两部分:

  1. 浏览器计算出给指定元素应用哪些类、伪选择器和 ID。
  2. 根据上一步的结果,计算元素的最终样式

那么可采取的措施:

  • 降低选择器的复杂性,比如 .box:nth-last-child(-n+1) .title {} 就不应该使用,改为一个类 .final-box-title {}。参考 BEM
  • 减少要计算样式的元素数量

布局优化

布局是浏览器计算各元素几何信息的过程:元素的大小以及在页面中的位置。 此过程在 Chrome、Opera、Safari 和 Internet Explorer 中称为布局 (Layout)。 在 Firefox 中称为自动重排 (Reflow)。

  • 布局几乎总是作用到整个文档,尽可能避免布局操作。
  • 使用 flexbox 而不是较早的布局模型,flexbox 的布局开销会小很多。(在任何情况下,不管是否选择 flexbox,都应当在应用的高压力点期间尝试完全避免触发布局!)

避免强制同步布局

通常将一帧送往屏幕会按照像素管道的顺序

但是,可以使用 JavaScript 强制浏览器提前执行布局。这被称为强制同步布局。

const logBoxHeight = () => {
box.classList.add('super-big')
console.log(box.offsetHeight)
}

现在,为了回答高度问题,浏览器必须先应用样式更改(由于增加了 super-big 类),然后运行布局。这时它才能返回正确的高度。

// example

本应在下一帧时得到的 offsetWidth 在这一帧被提前计算出来了,这是不必要的,并且可能是开销很大的工作。

// 不停的造成强制同步布局 const resizeAllParagraphsToMatchBlockWidth = () => {
arr.forEach(v => {
v.style.width = box.offsetWidth + 'px'
})
}

绘制优化

绘制是填充像素的过程,像素最终合成到用户的屏幕上。 它往往是管道中运行时间最长的任务,应尽可能避免此任务。除 transformopacity 属性之外,更改任何属性始终都会触发绘制。

不同的绘制开销也不一样,background: red; 会比 box-shadow: 0, 4px, 4px, rgba(0,0,0,0.5); 开销小。

如果您触发布局,则总是会触发绘制,因为更改任何元素的几何属性意味着其像素需要修正。如果更改非几何属性,例如背景、文本或阴影,也可能触发绘制。在这些情况下,不需要布局,并且管道将如下所示:

元素提升

绘制并非总是绘制到内存中的单个图像。事实上,在必要时浏览器可以绘制到多个图像或合成器层。可以在不影响其他元素的情况下进行处理,就像 Photoshop 的图层。

创建新层的最佳方式是使用 will-change CSS 属性,并且通过 transform 的值将创建一个新的合成器层:

.moving-element {
/* 不支持 will-change 但受益于层创建的浏览器可使用(滥用)3D 变形来强制创建一个新层 transform: translateZ(0); */
will-change: transform;
}

不要创建太多层,因为每层都需要内存和管理开销。

减少绘制区域

Sometimes, however, despite promoting elements, paint work is still necessary. A large challenge of paint issues is that browsers union together two areas that need painting, and that can result in the entire screen being repainted. So, for example, if you have a fixed header at the top of the page, and something being painted at the bottom the screen, the entire screen may end up being repainted.
Note: On High DPI screens elements that are fixed position are automatically promoted to their own compositor layer. This is not the case on low DPI devices because the promotion changes text rendering from subpixel to grayscale, and layer promotion needs to be done manually. Reducing paint areas is often a case of orchestrating your animations and transitions to not overlap as much, or finding ways to avoid animating certain parts of the page.

看不懂呢。。

输入处理程序去除抖动

在最快的情况下,当用户与页面交互时,页面的合成器线程可以获取用户的触摸输入并直接使内容移动。这不需要主线程执行任务,主线程执行的是 JavaScript、布局、样式或绘制。

当时当你添加一个输入处理程序,例如 touchstart,则处理器进程必须等待此处理程序完成,因为你可能调用 preventDefault() 来阻止触摸滚动的发生。即使没有调用 preventDefault(),合成器也必须等待,这样用户滚动会被阻止,这就可能导致卡顿和漏掉帧。

避免在在输入处理程序中更改样式

如果在处理程序中改变了样式,在 requestAnimationFrame 回调开始时就读取视觉属性的话,会触发强制同步布局。

利用 requestAnimationFrame 做 debounce

function onScroll (evt) {
  // Store the scroll value for laterz.
lastScrollY = window.scrollY;
  // Prevent multiple rAF callbacks.
if (scheduledAnimationFrame)
return;
  scheduledAnimationFrame = true;
requestAnimationFrame(readAndUpdatePage);
}
window.addEventListener('scroll', onScroll);

Originally published at gist.github.com.