原文:https://developers.google.com/web/updates/2018/09/inside-browser-part4
当用户输入来到合成器
这是关于Chrome内部工作原理系列文章的最后一章,这系列文章研究了浏览器如何将我们的代码处理成我们看到的网页。在前面的一章,我们学习了渲染进程和合成器。在这一章中,我们将看到合成器是如何流畅的响应用户的输入。
从浏览器视角出发的输入事件
当你听到“输入事件”,你可能仅仅想到文本的输入或者鼠标点击事件,但是从浏览器的视角看,输入意味着任何来自用户的指示,包括鼠标滚轮滚动、触摸、鼠标悬停等都是输入事件。
当用户在屏幕上触摸时,浏览器进程(browser process)首先接收到该事件,但是浏览器进程只能知道该事件发生的位置,这是因为选项卡中网页的内容是被渲染进程(renderer process)处理的。因此,浏览器进程会将事件类型和坐标信息发送给渲染进程,渲染进程通过查找事件目标对象和运行附着的事件监听器,就能够恰如其分地处理事件。
合成器接收输入事件
在上一篇文章中,我们了解了合成器如何通过合成光栅化图层来平滑地处理滚动事件,如果没有输入事件监听器添加到页面,那么合成器线程完全能够独立于主线程创建新的合成帧;但是如果一些事件监听器被添加到了页面呢?合成器线程怎么判断一个事件是否需要被处理呢?
认识非快速滚动区域(non-fast scrollable region)
因为运行JavaScript代码是主线程的工作,因此在合成页面时,合成器线程会将页面中附加有事件处理程序的区域标记为“非快速滚动区域”,通过获得这些信息,如果事件发生在标记的区域,合成器线程就知道将事件发送给主线程。如果输入事件来自标记区域之外,则合成器线程继续合成新帧,而无需等待主线程。
编写事件处理程序的时候请注意
web开发中常见的事件处理模式是事件委托,由于事件冒泡,您可以在最顶层元素附加一个事件处理程序,并根据event target委派任务。 您可能已经看到或写过了如下代码:
document.body.addEventListener('touchstart', event => {
if (event.target === area) {
event.preventDefault();
}
});
由于您只需为所有元素编写一个事件处理程序,因此这种事件委托模式的工效是很有吸引力的,但是,如果你从浏览器的角度来看这段代码,那么现在整个页面都被标记为非快速滚动区域,这意味着即使你的应用程序不关心来自页面某些部分的输入,然而每当事件发生时,合成器线程也必须跟主线程通信并等待它完成。这样一来,合成器的平滑滚动能力就被打破了。
为了缓解这种情况,您可以在事件侦听器中传递passive: true 选项, 这向浏览器暗示您仍然希望在主线程中侦听事件,但合成器也可以继续合成新帧。
document.body.addEventListener('touchstart', event => {
if (event.target === area) {
event.preventDefault()
}
}, {passive: true});
检测事件是否可以取消
想象一下,在页面中有一个框,你想让它仅能水平滚动,这个时候你在监听指定事件的时候传入passive: true选项使页面滚动流畅,但在页面进行垂直滚动开始时,为了阻止它,你可能会用到preventDefault,你可以使用event.cancelable来检测事件是否可以被阻止。
document.body.addEventListener('pointermove', event => {
if (event.cancelable) {
event.preventDefault(); // block the native scroll
/*
* do what you want the application to do here
*/
}
}, {passive: true});
或者,你可以使用CSS规则比如touch-action 去完全取消垂直方向的手势平移。
#area {
touch-action: pan-x;
}
如何找到事件的目标对象?
当合成器线程给主线程发送输入事件时,首先要做的就是运行命中测试去寻找事件目标对象。命中测试使用在渲染进程中生成的绘制记录数据来找出事件发生的点坐标下方的内容。
尽可能少的给主线程派发事件
在上一篇文章中,我们讨论了典型显示器每秒刷新屏幕 60 次,以及我们如何跟上节奏以获得流畅的动画,对于用户输入事件,典型的触摸屏设备每秒传递 60-120 次触摸事件,典型的鼠标每秒传递 100 次事件, 输入事件产生的频率超过了屏幕刷新的频率。
如果像 touchmove 这样的连续事件,每秒发送120次到主线程 ,那么这可能会触发过多的命中测试和JavaScript的执行,具体情况要取决于屏幕的刷新频率有多慢。
过多的事件会淹没帧的时间线,以至于造成卡顿。
为了尽量减少对主线程的过多调用,Chrome 会合并连续事件(例如wheel、mousewheel、mousemove、pointermove、touchmove)并将调度延迟到下一个requestAnimationFrame之前。
任何离散的事件,如keydown、keyup、mouseup、mousedown、touchstart和touchend都会立即分派。
使用getCoalescedEvents 来获取帧内事件
对于大多数Web应用程序,合并事件应该足以提供良好的用户体验。但是,如果您正在构建诸如绘图应用程序和基于touchmove坐标放置路径之类的东西,您可能会丢失中间坐标而难以绘制出平滑的线。在这种情况下,您可以在指定的事件中使用 getCoalescedEvents方法来获取有关这些合并事件的信息。
window.addEventListener('pointermove', event => {
const events = event.getCoalescedEvents();
for (let event of events) {
const x = event.pageX;
const y = event.pageY;
// draw a line using x and y coordinates.
}
});
下一步
在这一系列文章中,我们讲述了浏览器的内部工作原理。如果你没有思考过为什么DevTools推荐在你的事件处理器中传入{passive: true}选项,为什么在你的脚本script标签上加上async属性,我希望这个系列文章能阐明为什么浏览器需要这些信息来提供更快、更流畅的Web体验。
使用 Lighthouse
使用Lighthouse测试审查你的网站,Lighthouse生成的报告会告诉你怎么做是正确的,以及怎么提升你的网站体验。
学会怎么去测量网站性能
不同站点的性能调整可能会有所不同,因此衡量站点的性能并确定最适合您站点的方法至关重要。 Chrome DevTools 团队有少量的关于如何测量网站性能的教程。
总结
当我开始构建网站时,我几乎只关心如何编写代码以及如何提高工作效率。 这些东西很重要,但我们也应该考虑浏览器如何处理我们编写的代码。 现代浏览器一直并将继续投资于用更多的方法,为用户提供更好的体验。 组织好我们的代码对浏览器来说是非常好的,同时又可以改善用户的体验。 我希望你和我一起努力对探索写出对浏览器友好的代码!