Inside look at modern web browser 是介绍浏览器实现原理的系列文章,共 4 篇,本次精读介绍第三篇。
概述
本篇宏观的介绍 renderer process 做了哪些事情。
浏览器 tab 内 html、css、javascript 内容基本上都由 renderer process 的主线程处理,除了一些 js 代码会放在 web worker 或 service worker 内,所以浏览器主线程核心工作就是解析 web 三剑客并生成可交互的用户界面。
解析阶段
首先 renderer process 主线程会解析 HTML 文本为 DOM(Document Object Model),只译为中文就是文档对象模型,所以首先要把文本结构化才能继续处理。不仅是浏览器,代码的解析也得首先经历 Parse 阶段。
对于 HTML 的 link、img、script 标签需要加载远程资源的,浏览器会调用 network thread 优先并行处理,但遇到 script 标签就必须停下来优先执行,因为 js 代码可能会改变任何 dom 对象,这可能导致浏览器要重新解析。所以如果你的代码没有修改 dom 的副作用,可以添加 async、defer 标签,或 JS 模块的方式使浏览器不必等待 js 的执行。
样式计算
只有 DOM 是不够的,style 标签申明的样式需要作用在 DOM 上,所以基于 DOM,浏览器要生成 CSSOM,这个 CSSOM 主要是基于 css 选择器(selector)确定作用节点的。
布局
有了 DOM、CSSOM 仍然不足以绘制网页,因为我们仅知道结构和样式,但不知道元素的位置,这就需要生成 LayoutTree 以描述布局的结构。
LayoutTree 和 DOM 结构很像了,但比如 display: none
的元素不会出现在 LayoutTree 上,所以 LayoutTree 仅考虑渲染结构,而 DOM 是一个综合描述结构,它不适合直接用来渲染。
原文特别提到,LayoutTree 有个很大的技术难点,即排版,Chrome 专门有一整个团队在攻克这个技术难题。为什么排版这么难?可以从这几个例子中体会冰山一角:盒模型间碰撞、字体撑开内容导致换行,引发更大区域的重新排版、一个盒模型撑开挤压另一个盒模型,但另一个盒模型大小变化后内容排版也随之变化,导致盒模型再次变化,这个变化又导致了外部其它盒模型的布局变化。
布局最难的地方在于,需要对所有奇奇怪怪的布局定式做一个尽量合理的处理,而很多时候布局定式间规则是相互冲突的。而且这还不考虑布局引擎的修改在数亿网页上引发未知 BUG 的风险。
绘图
有了 DOM、CSSOM、LayoutTree 就够了吗?还不行,还缺少最后一环 PaintRecord,这个指绘图记录,它会记录元素的层级关系,以决定元素绘制的顺序。因为 LayoutTree 仅决定了物理结构,但不决定元素的上下空间结构。
有了 DOM、CSSOM、LayoutTree、PaintRecord 之后,终于可以绘图了。然而当 HTML 变化时,重绘的代价是巨大的,因为上面任何一步的计算结果都依赖前面一步,HTML 改变时,需要对 DOM、CSSOM、LayoutTree、PaintRecord 进行重新计算。
大部分时候浏览器都可以在 16ms 内完成,使 FPS 保持在 60 左右,但当页面结构过于复杂,这些计算本身超过了 16ms,或其中遇到 js 代码的阻塞,都会导致用户感觉到卡顿。当然对于 js 卡顿问题可以通过 requestAnimationFrame
把逻辑运算分散在各帧空闲时进行,也可以独立到 web worker 里。
合成
绘图的步骤称为 rasterizing(光栅化)。在 Chrome 最早发布时,采用了一种较为简单的光栅化方案,即仅渲染可是区域内的像素点,当滚动后,再补充渲染当前滚动位置的像素点。这样做会导致渲染永远滞后于滚动。
现在一般采用较为成熟的合成技术(compositing),即将渲染内容分层绘制与渲染,这可以大大提升性能,并可通过 CSS 属性 will-change
手动申明为一个新层(不要滥用)。
浏览器会根据 LayoutTree 分析后得到 LayerTree(层树),并根据它逐层渲染。
合成层会将绘图内容切分为多个栅格并交由 GPU 渲染,因此性能会非常好。
精读
从渲染分层看性能优化
本篇提到了浏览器渲染的 5 个重要环节:解析、样式、布局、绘图、合成,是前端开发者日常工作中对浏览器体感最深的部分,也是优化最长发生在的部分。
其实从性能优化角度来看,解析环节可以被替代为 JS 环节,因为现代 JS 框架往往没有什么 HTML 模版内容要解析,几乎全是 JS 操作 DOM,所以可以看作 5 个新环节:JS、样式、布局、绘图、合成。
值得注意的是,几乎每层的计算都依赖上层的结果,但并不是每层都一定会重复计算,我们需要尤其注意以下几种情况:
- 修改元素几何属性(位置、宽高等)会触发所有层的重新计算,因为这是一个非常重量级的修改。
- 修改某个元素绘图属性(比如颜色和背景色),并不影响位置,则会跳过布局层。
- 修改比如 transform 属性会跳过布局与绘图层,这看上去很不可思议。
对于第三点,由于 transform 的内容会提升到合成层并交由 GPU 渲染,因此并不会与浏览器主线程的布局、绘图放在一起处理,所以视觉上这个元素的确产生了位移,但它和修改 left
、top
的位移在实现上却有本质的不同。
所以站在浏览器开发者的角度,可以轻松理解为什么这种优化不是奇技淫巧了,因为本身浏览器的实现就把布局、绘图与合成层的行为分离开了,不同的代码底层方案不同,性能肯定会不同。你可以通过 csstriggers 查看不同 css 属性会引发哪些层的重计算。
当然作为开发者还是可以吐槽,为什么浏览器不能 “自动把 left
top
与 transform
的实现细节屏蔽,并自动进行合理的分层”,然而如果浏览器厂商做不到这一点,开发者还是主动去了解实现原理吧。
隐式合成层、层爆炸、层自动合并
除了 transform
、will-change
属性外,还有很多种情况元素会提升到合成层,比如 video
、canvas
、iframe
,或 fixed
元素,但这些都有明确的规则,所以属于显示合成。
而隐式合成是指元素没有被特别标记,但也被提升到合成层的情况,这种情况常见发生在 z-index
元素产生重叠时,下方的元素显示申明提升到合成层,则浏览器为了保证 z-index
覆盖关系,就要隐式把上方的元素提升到合成层。
层爆炸是指隐式合成的原因,当 css 出现一些复杂行为时(比如轨迹动画),浏览器无法实时捕捉哪些元素位于当前元素上方,所以只好把所有元素都提升到合成层,当合成层数量过多,主线程与 GPU 的通信可能会成为瓶颈,反而影响性能。
浏览器也会支持层自动合并,比如隐式提升到合成层时,多个元素会自动合并在一个合成层里。但这种方式也并不总是靠谱,自动处理毕竟猜不到开发者的意图,所以最好的优化方式是开发者主动干预。
我们只要注意将所有显示提升到合成层的元素放在 z-index
的上方,这样浏览器就有了判断依据,不用再担惊受怕会不会这个元素突然移动到某个元素的位置,导致压住了那个元素,于是又不得不把这个元素给隐式提升到合成层以保证它们之间顺序的正确性,因为这个元素本来就位于其它元素的最上方。
总结
读完这篇文章,希望你能根据浏览器在渲染进程的实现原理,总结出更多代码级别的性能优化经验。
最后想要吐槽的是,浏览器规范由于是逐步迭代的,因此看似都在描述位置的 css 属性其实背后实现原理是不同的,虽然这个规则体现在 W3C 规范上,但如果仅从属性名是很难看出来端倪的,因此想要做极致性能优化就必须了解浏览器实现原理。
讨论地址是: 精读《深入了解现代浏览器三》· Issue #379 · dt-fe/weekly
如果你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。
关注 前端精读微信公众号
版权声明:自由转载-非商用-非衍生-保持署名( 创意共享 3.0 许可证)