Inside look at modern web browser 是介绍浏览器实现原理的系列文章,共 4 篇,本次精读介绍第四篇。
概述
前几章介绍了浏览器的根底过程、线程以及它们之间协同的关系,并重点说到了渲染过程是如何解决页面绘制的,那么最初一章也就深刻到了浏览器是如何解决页面中事件的。
全篇站在浏览器实现的视角思考问题,十分乏味。
输出进入合成器
这是第一大节的题目。乍一看可能不明确在说什么,但这句话就是本文的外围知识点。为了更好的了解这句话,先要解释输出与合成器是什么:
- 输出:不仅包含输入框的输出,其实所有用户操作在浏览器眼中都是输出,比方滚动、点击、鼠标挪动等等。
- 合成器:第三节说过的,渲染的最初一步,这一步在 GPU 进行光栅化绘图,如果与浏览器主线程解耦的化效率会十分高。
所以输出进入合成器的意思是指,在浏览器理论运行的环境中,合成器不得不响应输出,这可能会导致合成器自身渲染被阻塞,导致页面卡顿。
“non-fast” 滚动区域
因为 js 代码能够绑定事件监听,而且事件监听中存在一种 preventDefault()
的 API 能够阻止事件的原生成果比方滚动,所以在一个页面中,浏览器会对所有创立了此监听的区块标记为 “non-fast” 滚动区域。
留神,只有创立了 onwheel
事件监听就会标记,而不是说调用了 preventDefault()
才会标记,因为浏览器不可能晓得业务什么时候调用,所以只能一刀切。
为什么这种区域被称为 “non-fast”?因为在这个区域触发事件时,合成器必须与渲染过程通信,让渲染过程执行 js 事件监听代码并取得用户指令,比方是否调用了 preventDefault()
来阻止滚动?如果阻止了就终止滚动,如果没有阻止才会持续滚动,如果最终后果是不阻止,但这个等待时间耗费是微小的,在低性能设施比方手机上,滚动提早甚至有 10~100ms。
然而这并不是设施性能差导致的,因为滚动是在合成器产生的,如果它能够不与渲染过程通信,那么即使是 500 元的安卓机也能够晦涩的滚动。
留神事件委托
更有意思的是,浏览器反对一种事件委托的 API,它能够将事件委托到其父节点一并监听。
这本是一个十分不便的 API,但对浏览器实现可能是一个劫难:
document.body.addEventListener('touchstart', event => {if (event.target === area) {event.preventDefault();
}
});
如果浏览器解析到下面的代码,只能用无语来形容。因为这意味着必须对全页面都进行 “non-fast” 标记,因为代码委托的是整个 document!这会导致滚动十分慢,因为在页面任何中央滚动都要产生一次合成器与渲染过程的通信。
所以最好的方法就是不要写这种监听。但还有一种计划是,通知浏览器你不会 preventDefault()
,这是因为 chrome 通过对利用源码统计后发现,大概 80% 的事件监听没有 preventDefault()
,而仅仅是做别的事件,所以合成器应该能够与渲染过程的事件处理并行进行,这样既不卡顿,逻辑也不会失落。所以增加了一种 passive: true
的标记,标识以后事件能够并行处理:
document.body.addEventListener('touchstart', event => {if (event.target === area) {event.preventDefault()
}
}, {passive: true});
这样就不会卡顿了,但 preventDefault()
也会生效。
查看事件是否可勾销
对于 passive: true
的状况,事件就实际上变得不可勾销了,所以咱们最好在代码里做一层判断:
document.body.addEventListener('touchstart', event => {if (event.cancelable && event.target === area) {event.preventDefault()
}
}, {passive: true});
然而这仅仅是阻止执行没有意义的 preventDefault()
,并不能阻止滚动。这种状况下,最好的方法是通过 css 申明来阻止横向挪动,因为这个判断不会产生在渲染过程,所以不会导致合成器与渲染过程的通信:
#area {touch-action: pan-x;}
事件合并
因为事件触发频率可能比浏览器帧率还要高(1 秒 120 次),如果浏览器保持对每个事件都进行响应,而一次事件都必须在 js 里响应一次的话,会导致大量事件阻塞,因为当 FPS 为 60 时,一秒也仅能执行 60 次事件响应,所以事件积压是无奈防止的。
为了解决这个问题,浏览器在针对可能导致积压的事件,比方滚动事件时,将多个事件合并到一次 js 中,仅保留最终状态。
如果不心愿丢掉事件两头过程,能够应用 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.
}
});
精读
只有咱们意识到事件监听必须运行在渲染过程,而古代浏览器许多高性能“渲染”其实都在合成层采纳 GPU 做,所以看上去不便的事件监听必定会拖慢页面晦涩度。
但就这件事在 React 17 中有过一次探讨 Touch/Wheel Event Passiveness in React 17(实际上在行将到来的 18 该问题还在探讨中 React 18 not passive wheel / touch event listeners support),因为 React 能够间接在元素上监听 Touch、Wheel 事件,但其实框架采纳了委托的形式在 document(后在 app 根节点)对立监听,这就导致了用户基本无从决定事件是否为 passive
,如果框架默认 passive
,会导致 preventDefault()
生效,否则性能得不到优化。
就论断而言,React 目前还是对几个受影响的事件 touchstart
touchmove
wheel
采纳 passive
模式,即:
const Test = () => (
<div
// 没有用的,无奈阻止滚动,因为委托处默认 passive
onWheel={event => event.preventDefault()}
>
...
</div>
)
尽管论断如此而且对性能敌对,但并不是一个让所有人都能称心的计划,咱们看看过后 Dan 是如何思考,并给了哪些解决方案的。
首先背景是,React 16 事件委托绑定在 document 上,React 17 事件委托绑定在 App 根节点上,而依据 chrome 的优化,绑定在 document 的事件委托默认是 passive
的,而其它节点的不会,因而对 React 17 来说,如果什么都不做,仅扭转绑定节点地位,就会存在一个 Break Change。
- 第一种计划是保持 Chrome 性能优化的精力,委托时仍然 pasive 解决。这样解决至多和 React 16 一样,
preventDefault()
都是生效的,尽管不正确,但至多不是 BreakChange。 - 第二种计划即什么都不做,这导致本来默认
passive
的因为绑定到非 document 节点上而non-passive
了,这样做不仅有性能问题,而且 API 会存在 BreackChange,尽管这种做法更“原生”。 - touch/wheel 不再采纳委托,意味着浏览器能够有更少的 “non-fast” 区域,而
preventDefault()
也能够失效了。
最终抉择了第一个计划,因为临时不心愿在 React API 层面呈现行为不统一的 BreakChange。
然而 React 18 是一次 BreakChange 的机会,目前还没有进一步定论。
总结
从浏览器角度对待问题会让你具备上帝视角而不是开发者视角,你不会再感觉一些奇奇怪怪的优化逻辑是 Hack 了,因为你理解浏览器背地是如何了解与实现的。
不过咱们也会看到一些和实现强绑定的无奈,在前端开发框架实现时造成了不可避免的困扰。毕竟作为一个不理解浏览器实现的开发者,天然会认为 preventDefault()
绑定在滚动事件时,肯定能够阻止默认滚动行为呀,但为什么因为:
- 浏览器分为合成层和渲染过程,通信老本较高导致滚动事件监听会引发滚动卡顿。
- 为了防止通信,浏览器默认为 document 绑定开启
passive
策略缩小 “non-fast” 区域。 - 开启了
passive
的事件监听preventDefault()
会生效,因为这层实现在 js 里而不是 GPU。 - React16 采纳事件代理,把元素
onWheel
代理到 document 节点而非以后节点。 - React17 将 document 节点绑定下移到了 App 根节点,因而浏览器优化后的
passive
生效了。 - React 为了放弃 API 不产生 BreakChange,因而将 App 根节点绑定的事件委托默认补上了
passive
,使其体现与绑定在 document 一样。
总之就是 React 与浏览器实现背地的纠纷,导致滚动行为阻止生效,而这个后果链条传导到了开发者身上,而且有显著感知。但理解背地起因后,你应该能了解一下 React 团队的苦楚吧,因为已有 API 的确没有方法形容是否 passive
这个行为,所以这是个临时无奈解决的问题。
探讨地址是:精读《深刻理解古代浏览器四》· Issue #381 · dt-fe/weekly
如果你想参加探讨,请 点击这里,每周都有新的主题,周末或周一公布。前端精读 – 帮你筛选靠谱的内容。
关注 前端精读微信公众号
<img width=200 src=”https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg”>
版权申明:自在转载 - 非商用 - 非衍生 - 放弃署名(创意共享 3.0 许可证)