关于前端:精读深入了解现代浏览器四

51次阅读

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

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。

  1. 第一种计划是保持 Chrome 性能优化的精力,委托时仍然 pasive 解决。这样解决至多和 React 16 一样,preventDefault() 都是生效的,尽管不正确,但至多不是 BreakChange。
  2. 第二种计划即什么都不做,这导致本来默认 passive 的因为绑定到非 document 节点上而 non-passive 了,这样做不仅有性能问题,而且 API 会存在 BreackChange,尽管这种做法更“原生”。
  3. 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 许可证)

正文完
 0