让页面滑动流畅得飞起的新特性:Passive Event Listeners

在不久前的Google I/O 2016 Mobile Web Talk中,Google公布了一个让页面滑动更流畅的新特性Passive Event Listeners。该特性目前已经集成到Chrome51版本中。

Chrome51上使用Passive Event Listener特性前后的效果对比 链接地址 从效果对比视频中可以明显看到,使用Passive Event Listeners特性后,页面的滑动流畅度相对使用之前提升了很多。

看完Passive Event Listeners特性这么给力的效果后,相信大部分童鞋脑海中都会产生以下几个问题:

  1. Passive Event Listeners是什么?
  2. 为什么需要Passive Event Listeners?
  3. Passive Event Listeners是怎么实现的?

接下来,我们将围绕上面的这3个问题来深入理解Passive Event Listeners特性。

Passive Event Listeners是什么?

Passive event listeners are a new feature in the DOM spec that enable developers to opt-in to better scroll performance by eliminating the need for scrolling to block on touch and wheel event listeners. Developers can annotate touch and wheel listeners with {passive: true} to indicate that they will never invoke preventDefault.

Passive Event Listeners是Chrome提出的一个新的浏览器特性:Web开发者通过一个新的属性passive来告诉浏览器,当前页面内注册的事件监听器内部是否会调用preventDefault函数来阻止事件的默认行为,以便浏览器根据这个信息更好地做出决策来优化页面性能。当属性passive的值为true的时候,代表该监听器内部不会调用preventDefault函数来阻止默认滑动行为,Chrome浏览器称这类型的监听器为被动(passive)监听器。目前Chrome主要利用该特性来优化页面的滑动性能,所以Passive Event Listeners特性当前仅支持mousewheel/touch相关事件。

如下面的Html代码中,页面通过调用document.addEventListener来添加一个mousewheel事件的监听器handler,并通过设置passive属性的值为true来声明监听器handler是被动监听mousewheel事件,即handler内部不会调用事件的preventDefault函数。

为什么需要Passive Event Listeners特性?

Passive Event Listeners特性是为了提高页面的滑动流畅度而设计的,页面滑动流畅度的提升,直接影响到用户对这个页面最直观的感受。这个不难理解,想象一下你想要滑动某个页面浏览内容,当你用鼠标滚轮或者用手指触摸屏幕上下滑动的时候,页面并没有按你的预期进行滚动,此时你内心往往会感觉到一丝不爽,甚至想放弃该页面。Facebook之前做了一项试验,他们将页面滑动的响应刷新率从60FPS降低到30FPS的时候,发现用户的参与度急速下降。

由前面对Passive Event Listeners特性的介绍可知,Passive Event Listenrers特性是让Web开发者来告诉浏览器,当前页面内注册的mousewheel/touch事件监听器是否属于被动监听器,以便让浏览器更好地做决策来提高页面的滑动流畅度。那么Chrome浏览器为什么需要知道是否被动监听器这个信息呢?浏览器知道这个信息之后,它要做什么决策呢?要回答这个问题,有必要先了解一下目前Chrome浏览器的线程化渲染框架,它是Passive Event Listeners特性的基础。

在介绍Chrome浏览器的线程化渲染框架之前,我们先来简单了解本文涉及到的Chrome浏览器的一些概念。

  1. 绘制(Paint):将绘制操作转换成为图像的过程(比如软件模式下经过光栅化生成位图,硬件模式下经过光栅化生成纹理)。在Chrome中,绘制分为两部分实现:绘制操作记录部分(main-thread side)和绘制实现部分(impl-side)。绘制记录部分将绘制操作记录到SKPicture中,绘制实现部分负责将SKPicture进行光栅化转成图像;
  2. 图层(Paint Layer):在Chrome中,页面的绘制是分层绘制的,页面内容变化的时候,浏览器仅需要重新绘制内容变化的图层,没有变化的图层不需要重新绘制;
  3. 合成(Composite):将绘制好的图层图像混合在一起生成一张最终的图像显示在屏幕上的过程;
  4. 渲染(Render):绘制+合成=渲染;
  5. UI线程(UI Thread):浏览器的主线程,负责接收到系统派发给浏览器窗口的事件,资源下载等;
  6. 内核线程(Main/Render Thread):Blink内核及V8引擎运行的线程,如DOM树构建,元素布局,绘制(main-thread side),JavaScript执行等逻辑在该线程中执行;
  7. 合成线程(Compositor Thread):负责图像合成的线程,如绘制(impl-side),合成等逻辑在该线程中执行。

OK,了解完上面的几个概念后,我们正式开始Chrome线程渲染框架的介绍。

Chrome浏览器的线程化渲染框架

我们回顾一下传统的单线程渲染框架,如下图所示,内核线程几乎包揽了页面内容渲染的所有工作,如JavaScript执行,元素布局,图层绘制,图层图像合成等,每项工作的执行耗时基本都跟页面内容相关,耗时一般在几十毫秒至几百毫秒不等。

对于这种单线程渲染框架,存在两个明显的问题:

  1. 流水线的执行方式,后面的工作必须等待前面工作执行完成才能处理,无法将相互独立的工作并行处理;
  2. 内核线程负责的工作太多且耗时,一旦遇上内核在执行耗时较长的工作,用户的输入事件是将无法立即得到响应的。

对于第1个问题,浏览器很难控制页面从内容变化到布局渲染整个过程的耗时(即新生成一帧内容的耗时),中间任何一项工作的执行都可能导致整体过程耗时变大,过大的耗时会导致页面内容的刷新率偏低,从而形成视觉上的卡顿。如浏览器收到VSync中断信号通知的时候,意味着页面需要立即对内容进行渲染,但这个时候内核线程可能还在执行一些业务的JavaScript代码,导致页面内容的渲染无法立即开始,如果页面无法在下一个VSync中断信号到来之前完成对内容的渲染,则页面会出现丢帧,用户感觉到页面操作出现卡顿。

注:VSync信号中断的频率,一般跟设备屏幕的刷新率对齐,比如设备的刷新率为60FPS(Frames Per Second),那么大概16.67ms会触发一下Vsync中断信号。Chrome浏览器和Android系统等都是通过VSync中断信号来通知页面启动内容的渲染(BeginFrame)。

对于第2个问题,由于内核线程负责的工作太多,这将导致内核线程经常处于忙碌状态,无法快速处理外界的输入消息,表现为用户操作了页面,但是无法立即得到响应。

为了优化第1个问题,Chrome浏览器对内核线程负责的工作进行拆分,通过多线程并发处理提高渲染效率减少丢帧,如内核线程仅负责DOM树构建、元素的布局、图层绘制记录部分(main-thread side)、JavaScript的执行,而图层绘制实现部分(impl-side)、图层图像合成则是交给合成线程负责处理。这种多线程负责页面内容的渲染的框架,在Chrome中称为线程化渲染框架(Threaded Compositor Architecture)。

如上图所示,在Chrome的线程化渲染框架中,当内核线程完成第1帧(Frame#1)的布局和记录绘制操作,立即通知合成线程对第一帧(Frame#1)进行渲染,然后内核线程就开始准备第2帧(Frame#2)的布局和记录绘制操作。由此可以看出,内核线程在进行第N+1帧的布局和记录绘制操作同时,合成线程也在努力进行第N帧的渲染并交给屏幕展示,这里利用了CPU多核的特性进行并发处理,因此提高了页面的渲染效率。由此也可知,实际上用户看到的页面内容,是上一帧的内容快照,新的一帧还在处理中。

要优化第2个问题,对浏览器来说非常困难的。只要输入事件要在内核线程执行逻辑,那么遇到内核线程在忙,必然无法立即得到响应。如用户的大部分输入事件都跟页面元素有关系,一旦页面元素注册了对应事件的监听器,监听器的逻辑代码(JavaScript)必须在内核线程中执行(V8引擎是运行在内核线程),因此这种输入事件经常无法立即得到响应的。

由上面的分析知道,用户的输入事件无法立即得到响应,是因为需要派发给内核线程处理。那有没有一些输入事件是可以不经过内核线程就能被快速处理的呢?答案是肯定的。

在Chrome中,这类可以不经过内核线程就能快速处理的输入事件为手势输入事件(滑动、捏合),手势输入事件是由用户连续的普通输入事件组合产生,如连续的mousewheel/touchmove事件可能会生成GestureScrollBegin/GestureScrollUpdate等手势事件。手势输入事件可以直接在已经渲染好的内容快照上操作,如滑动手势事件,直接对页面已经渲染好的内容快照进行滑动展示即可。由于线程化渲染框架的支持,手势输入事件可以不经过内核线程,直接由合成线程在内容快照上直接处理,所以即使此时内核线程在忙碌,用户的手势输入事件也是可以马上得到响应的。大家可以搞一个简单的demo验证一下Chrome浏览器的这个特性:如在一个有滚动条的页面内通过JavaScript执行一段死循环的代码(while-true之类的),这个时候再去尝试上下滑动页面,你会发现此时页面仍能流畅地滑动。

由此可知,Chrome浏览器对于手势输入事件的响应是非常快的,因为它可以不需要经过内核线程,直接由合成线程快速处理。然而手势输入事件的产生可能需要内核线程,这会导致Chrome对手势输入事件的优化效果大打折扣。由前面介绍知道,手势输入事件是由连续的普通输入事件组成,而这些普通的输入事件可能会被对应的事件监听器内部调用preventDefault函数来阻止掉事件的默认行为,在这种场景下是不会产生手势输入事件。如连续的mousewheel事件默认可以产生GestureScrollUpdate事件,但是如果监听器内部调用了preventDefault函数,那么这种情况下则不应该产生GestureScrollUpdate手势事件的。浏览器只有等内核线程执行到事件监听器对应的JavaScript代码时,才能知道内部是否会调用preventDefault函数来阻止事件的默认行为,所以浏览器本身是没有办法对这种场景进行优化的。这种场景下,用户的手势事件无法快速产生,会导致页面无法快速执行滑动逻辑,从而让用户感觉到页面卡顿。

而Chrome团队从统计数据中分析得出,注册了mousewheel/touch相关事件监听器的页面中,80%的页面内部都不会调用preventDefault函数来阻止事件的默认默认行为。对于这80%的页面,即使监听器内部什么都没有做,相对没有注册mousewheel/touch事件监听器的页面,在滑动流畅度上,有10%的页面增加至少100ms的延迟,1%的页面甚至增加500ms以上的延迟。Chrome团队认为对于统计中的这80%的页面来说,他们都是不希望因为注册mousewheel/touch相关事件监听器而导致滑动延迟增加的。点击这里 可以体验页面注册后导致的滑动延迟,如上图。

如果能让Web开发者来明确告诉浏览器,监听器内部不会调用preventDefault函数来禁止默认的事件行为,那么浏览器将能快速生成手势输入事件,从而让页面响应更快。

介绍完这里,大家应该明白Chrome浏览器为什么需要Passive Event Listeners特性了。接下来,我们来看看Passive Event Listeners特性是怎么实现的。

Passive Event Listeners的实现

为了更好地理解Passive Event Listeners特性,我们接下来了解一下它的实现过程。如上面代码所示,假定页面中注册了mousewheel事件的被动监听器,此时用户开始滑动鼠标滚轮来滑动页面。

如上图所述,用户的鼠标滚轮事件(WM_MouseWheel)由操作系统内核捕捉后,操作系统会将该事件派发给浏览器的UI线程处理。UI线程内部将系统的WM_MouseWheel事件转换为Chrome的WebInputEvent::MouseWheel事件后,接着通过IPC通道派发给合成线程的输入事件处理器处理。

合成线程的输入事件处理器收到WebInputEvent::MouseWheel事件后,内部先会查询MouseWheel事件监听器的类型属性,然后根据监听器的类型属性值来进行不同逻辑的处理。

目前Chrome中监听器的类型属性值主要有四种:EventListenerProperties::kNone,EventListenerProperties::kPassive,EventListenerProperties::kBlocking,EventListenerProperties::kBlockingAndPassive,如下代码所述。

在Chrome中,kBlocking和kBlockingAndPassive类型属性的处理逻辑是一样的,这个不难理解,只要存在一个非passive类型的事件监听器,那么都有可能阻止事件的默认行为。接下来,我们了解一下不同类型属性监听器的实现逻辑。

场景1: EventListenerProperties::kNone类型

当事件监听器的类型属性为EventListenerProperties::kNone时,意味着当前页面内没有注册对应事件的监听器。对于这种场景(如上图中的MouseWheel Handlers:No分支),合成线程会马上发送一个MouseWheel的ACK消息给UI线程,UI线程收到MouseWheel的ACK消息后,会判断该事件是否被消费(Comsumed,即调用了preventDefault),如果已经被消费,则什么都不做。否则,UI线程会产生一个滑动手势事件(如果当前不是在滑动过程,手势事件为GestureScrollBegin,否则为GestureScrollUpdate),并滑动手势事件通过IPC通道派发给合成线程处理,合成线程收到该滑动手势事件之后,直接对内容快照进行滑动处理,并展示给到屏幕上。这种场景下,由于没有涉及到内核线程处理,用户的输入响应会非常及时。

场景2: EventListenerProperties::kBlockingAndPassive 或 cc::EventListenerProperties::kBlocking类型

当事件监听器的类型属性为EventListenerProperties::kBlockingAndPassive或EventListenerProperties::kBlocking时,意味着当前页面至少存在一个非passive类型的事件监听器。对应这种场景(如上图中的MouseWheel Handlers:YES-Passive:No分支),合成线程无法知道对应的监听器内部是否会调用preventDefault函数来阻止默认行为,此时合成线程只能将该输入事件派发给内核线程处理(Dispatch Event to Main Thread)。等内核线程执行完监听器的处理逻辑后(Run JS Handler),再发送一个MouseWheel的ACK消息给UI线程,UI线程收到Mouse Wheel的ACK消息后的处理逻辑跟场景1一致。这种场景下,手势输入事件必须等待事件监听器逻辑处理完成后才会产生并派发给合成线程处理,由于事件监听器逻辑的执行时机不确定,将非常容易导致用户的输入事件无法立即响应。

场景3: EventListenerProperties::kPassive类型

当事件监听器的类型属性为EventListenerProperties::kPassive时,意味着当前页面只存在passive类型的事件监听器。对于这种场景(如上图中的MouseWheel Handlers:YES-Passive:YES分支),合成线程首先会发送一个MouseWheel的ACK消息给UI线程,执行跟场景1中一样的逻辑,同时将该事件派发给内核线程处理,执行跟场景2相似的逻辑,但是在Run JS Handlers完成后,不会再发送Mouse Wheel事件的ACK消息。这种场景下,实际上是场景2和场景3的组合,两个场景是并行处理的,因此用户的MouseWheel输入事件能会被立刻响应,也不会受到内核线程的事件监听器处理逻辑影响。

对于场景1和场景3的滑动,在Chrome中称为fast scroll模式,而场景2则称为slow scroll模式。

总结

经过上面的分析,我们了解到了Passive Event Listeners特性是什么,Passive Event Listeners特性产生的背景及Passive Event Listeners特性的实现逻辑,这其中涉及到了Chrome的多线程渲染框架、输入事件处理等知识。

文章来自公众号:小时光公众号(Tech Teahouse)

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

陈志兴的专栏

1 篇文章1 人订阅

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏iOS技术

iOS架构:Proxy实现局部模块化(附Demo)

在 iOS 开发中,Proxy 设计模式的体现更多的是以单个代理的方式,笔者为了优雅的达到更深层次的模块化,基于消息转发间接的实现多代理。

2847
来自专栏施炯的IoT开发专栏

《101 Windows Phone 7 Apps》读书笔记-Weight Tracker

课程内容 Ø Charts & Graphs     你平时关注自己的体重吗?Weight Tracker使得你可以随时跟踪自己的体重,并且提供几种体重发展趋...

1768
来自专栏coding...

Flutter 简易新闻项目目标效果对比简介代码代码地址

使用flutter快速开发 Android 和 iOS 的简易的新闻客户端 API使用的是 showapi(易源数据) 加载热门微信文章

512
来自专栏Android源码框架分析

十分钟了解Android触摸事件原理(InputManagerService)

从手指接触屏幕到MotionEvent被传送到Activity或者View,中间究竟经历了什么?Android中触摸事件到底是怎么来的呢?源头是哪呢?本文就直观...

1963
来自专栏菩提树下的杨过

flash 显示 qq客服状态

前几天看到有园友写了一篇“ flash查看对方qq是否在线 ”,正好今天有一个朋友搞flash全站,想使用这个功能,但是有些小要求,点击图标后,要求弹出QQ对话...

16610
来自专栏Linux驱动

18.Llinux-触摸屏驱动(详解)

本节的触摸屏驱动也是使用之前的输入子系统 1.先来回忆之前第12节分析的输入子系统 其中输入子系统层次如下图所示, ? 其中事件处理层的函数都是通过input_...

2219
来自专栏AhDung

【C#】组件发布:MessageTip,轻快型消息提示窗

原文和网盘demo我就不更新了,项目已开源到如下几处,有兴趣的朋友请关注,欢迎fork/push/pull:

612
来自专栏mukekeheart的iOS之旅

iOS学习——iOS原生实现二维码扫描

  最近项目上需要开发扫描二维码进行签到的功能,主要用于开会签到的场景,所以为了避免作弊,我们再开发时只采用直接扫描的方式,并且要屏蔽从相册读取图片,此外还在二...

38015
来自专栏葡萄城控件技术团队

如何在施工物料管理Web系统中处理大量数据并显示

最近在开发施工物料管理系统,其中涉及大量的物料信息需要管理和汇总,数据量非常庞大。之前尝试自己通过将原始数据,加工处理建模,在后台代码中通过分组、转置再显示到 ...

20010
来自专栏张善友的专栏

Metro风格XAML应用程序性能技巧

微软发布了一篇名为《Metro风格XAML应用程序性能技巧》的白皮书,其中包含一些关于保持响应、确保流畅动画、改善启动时间、消耗较少资源等方面的建议。我们在这里...

1748

扫码关注云+社区