极客时间已完结课程限时免费阅读

浏览器:一个浏览器是如何工作的?(阶段五)

浏览器:一个浏览器是如何工作的?(阶段五)-极客时间

浏览器:一个浏览器是如何工作的?(阶段五)

讲述:winter

时长11:35大小10.59M

你好,我是 winter。我们的浏览器系列已经进行到最后一篇。
在之前的几篇文章中,我们已经经历了把 URL 变成字符流,把字符流变成词(token)流,把词(token)流构造成 DOM 树,把不含样式信息的 DOM 树应用 CSS 规则,变成包含样式信息的 DOM 树,并且根据样式信息,计算了每个元素的位置和大小。
那么,我们最后的步骤,就是根据这些样式信息和大小信息,为每个元素在内存中渲染它的图形,并且把它绘制到对应的位置。

渲染

首先我们来谈谈渲染这个词,渲染也是个外来词,它是英文词 render 的翻译,render 这个词在英文里面,有“导致”“变成”的意思,也有“粉刷墙壁”的意思。
在计算机图形学领域里,英文 render 这个词是一个简写,它是特指把模型变成位图的过程。我们把 render 翻译成“渲染”,是个非常有意思的翻译,中文里“渲染”这个词是一种绘画技法,是指沾清水把墨涂开的意思。
所以,render 翻译成“渲染”,我认为是非常高明的,对 render 这个过程,用国画的渲染手法来概括,是颇有神似的。
我们现在的一些框架,也会把“从数据变成 HTML 代码的过程”称为 render,其实我觉得这是非常具有误导性的,我个人是非常不喜欢这种命名方式,当然了,所谓“文无第一”,在自然语言的范围里,我们很难彻底否定这种用法的合理性。
不过,在本篇文章中,我们可以约定一下,本文中出现的“渲染”一词,统一指的是它在图形学的意义,也就是把模型变成位图的过程。
这里的位图就是在内存里建立一张二维表格,把一张图片的每个像素对应的颜色保存进去(位图信息也是 DOM 树中占据浏览器内存最多的信息,我们在做内存占用优化时,主要就是考虑这一部分)。
浏览器中渲染这个过程,就是把每一个元素对应的盒变成位图。这里的元素包括 HTML 元素和伪元素,一个元素可能对应多个盒(比如 inline 元素,可能会分成多行)。每一个盒对应着一张位图。
这个渲染过程是非常复杂的,但是总体来说,可以分成两个大类:图形和文字。
盒的背景、边框、SVG 元素、阴影等特性,都是需要绘制的图形类。这就像我们实现 HTTP 协议必须要基于 TCP 库一样,这一部分,我们需要一个底层库来支持。
一般的操作系统会提供一个底层库,比如在 Android 中,有大名鼎鼎的 Skia,而 Windows 平台则有 GDI,一般的浏览器会做一个兼容层来处理掉平台差异。
这些盒的特性如何绘制,每一个都有对应的标准规定,而每一个的实现都可以作为一个独立的课题来研究,当年圆角 + 虚线边框,可是难倒了各个浏览器的工程师。考虑到这些知识互相都比较独立,对前端工程师来说也不是特别重要的细节,我们这里就不详细探究了。
盒中的文字,也需要用底层库来支持,叫做字体库。字体库提供读取字体文件的基本能力,它能根据字符的码点抽取出字形。
字形分为像素字形和矢量字形两种。通常的字体,会在 6px 8px 等小尺寸提供像素字形,比较大的尺寸则提供矢量字形。矢量字形本身就需要经过渲染才能继续渲染到元素的位图上去。目前最常用的字体库是 Freetype,这是一个 C++ 编写的开源的字体库。
在最普遍的情况下,渲染过程生成的位图尺寸跟它在上一步排版时占据的尺寸相同。
但是理想和现实是有差距的,很多属性会影响渲染位图的大小,比如阴影,它可能非常巨大,或者渲染到非常遥远的位置,所以为了优化,浏览器实际的实现中会把阴影作为一个独立的盒来处理。
注意,我们这里讲的渲染过程,是不会把子元素绘制到渲染的位图上的,这样,当父子元素的相对位置发生变化时,可以保证渲染的结果能够最大程度被缓存,减少重新渲染。

合成

合成是英文术语 compositing 的翻译,这个过程实际上是一个性能考量,它并非实现浏览器的必要一环。
我们上一小节中讲到,渲染过程不会把子元素渲染到位图上面,合成的过程,就是为一些元素创建一个“合成后的位图”(我们把它称为合成层),把一部分子元素渲染到合成的位图上面。
看到这句话,我想你一定会问问题,到底是为哪些元素创建合成后的位图,把哪些子元素渲染到合成的位图上面呢?
这就是我们要讲的合成的策略。我们前面讲了,合成是一个性能考量,那么合成的目标就是提高性能,根据这个目标,我们建立的原则就是最大限度减少绘制次数原则。
我们举一个极端的例子。如果我们把所有元素都进行合成,比如我们为根元素 HTML 创建一个合成后的位图,把所有子元素都进行合成,那么会发生什么呢?
那就是,一旦我们用 JavaScript 或者别的什么方式,改变了任何一个 CSS 属性,这份合成后的位图就失效了,我们需要重新绘制所有的元素。
那么如果我们所有的元素都不合成,会怎样呢?结果就是,相当于每次我们都必须要重新绘制所有的元素,这也不是对性能友好的选择。
那么好的合成策略是什么呢,好的合成策略是“猜测”可能变化的元素,把它排除到合成之外。
我们来举个例子:
<div id="a">
<div id="b">...</div>
<div id="c" style="transform:translate(0,0)"></div>
</div>
假设我们的合成策略能够把 a、b 两个 div 合成,而不把 c 合成,那么,当我执行以下代码时:
document.getElementById("c").style.transform = "translate(100px, 0)";
我们绘制的时候,就可以只需要绘制 a 和 b 合成好的位图和 c,从而减少了绘制次数。这里需要注意的是,在实际场景中,我们的 b 可能有很多复杂的子元素,所以当合成命中时,性能提升收益非常之高。
目前,主流浏览器一般根据 position、transform 等属性来决定合成策略,来“猜测”这些元素未来可能发生变化。
但是,这样的猜测准确性有限,所以新的 CSS 标准中,规定了 will-change 属性,可以由业务代码来提示浏览器的合成策略,灵活运用这样的特性,可以大大提升合成策略的效果。

绘制

绘制是把“位图最终绘制到屏幕上,变成肉眼可见的图像”的过程,不过,一般来说,浏览器并不需要用代码来处理这个过程,浏览器只需要把最终要显示的位图交给操作系统即可。
一般最终位图位于显存中,也有一些情况下,浏览器只需要把内存中的一张位图提交给操作系统或者驱动就可以了,这取决于浏览器运行的环境。不过无论如何,我们把任何位图合成到这个“最终位图”的操作称为绘制。
这个过程听上去非常简单,这是因为在前面两个小节中,我们已经得到了每个元素的位图,并且对它们部分进行了合成,那么绘制过程,实际上就是按照 z-index 把它们依次绘制到屏幕上。
然而如果在实际中这样做,会带来极其糟糕的性能。
有一个一度非常流行于前端群体的说法,讲做 CSS 性能优化,应该尽量避免“重排”和“重绘”,前者讲的是我们上一课的排版行为,后者模糊地指向了我们本课程三小节讲的三个步骤,而实际上,这个说法大体不能算错,却不够准确。
因为,实际上,“绘制”发生的频率比我们想象中要高得多。我们考虑一个情况:鼠标划过浏览器显示区域。这个过程中,鼠标的每次移动,都造成了重新绘制,如果我们不重新绘制,就会产生大量的鼠标残影。
这个时候,限制绘制的面积就很重要了。如果鼠标某次位置恰巧遮盖了某个较小的元素,我们完全可以重新绘制这个元素来完成我们的目标,当然,简单想想就知道,这种事情不可能总是发生的。
计算机图形学中,我们使用的方案就是“脏矩形”算法,也就是把屏幕均匀地分成若干矩形区域。
当鼠标移动、元素移动或者其它导致需要重绘的场景发生时,我们只重新绘制它所影响到的几个矩形区域就够了。比矩形区域更小的影响最多只会涉及 4 个矩形,大型元素则覆盖多个矩形。
设置合适的矩形区域大小,可以很好地控制绘制时的消耗。设置过大的矩形会造成绘制面积增大,而设置过小的矩形则会造成计算复杂。
我们重新绘制脏矩形区域时,把所有与矩形区域有交集的合成层(位图)的交集部分绘制即可。

结语

在这一节课程中,我们讲解了浏览器中的位图操作部分,这包括了渲染、合成和绘制三个部分。渲染过程把元素变成位图,合成把一部分位图变成合成层,最终的绘制过程把合成层显示到屏幕上。
当绘制完成时,就完成了浏览器的最终任务,把一个 URL 最后变成了一个可以看的网页图像。当然了,我们对每一个部分的讲解,都省略了大量的细节,比如我们今天讲到的绘制,就有意地无视了滚动区域。
尽管如此,对浏览器工作原理的感性认识,仍然可以帮助我们理解很多前端技术的设计和应用技巧,浏览器的工作原理和性能部分非常强相关,我们在实践部分的性能优化部分,会再次跟你做一些探讨。
实际上,如果你认真阅读浏览器系列的课程,是可以用 JavaScript 实现一个玩具浏览器的,我非常希望学习课程的同学中能有人这样做,一旦你做到了,收益会非常大。这就是我今天留给你的课外作业,你可以尝试一下。
分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 12

提建议

上一篇
浏览器:一个浏览器是如何工作的?(阶段四)
下一篇
浏览器DOM:你知道HTML的节点有哪几种吗?
 写留言

精选留言(29)

  • 大漠
    2019-02-20
    重绘,重排~都蛋疼

    编辑回复: 老师好呀,谢谢留言支持呀。

    共 2 条评论
    51
  • 逆风
    2019-02-20
    浏览器渲染出的结果如果是位图,请问输入框是怎么实现的?

    作者回复: 这个问到点子上啦,这块太复杂我故意没讲,实际上渲染过程除了位图,最终绘制上去还产生一个"热区",这个“热区”不但跟你说的input相关,还跟用户选择、鼠标事件和scroll等交互相关。

    43
  • 扩散性百万咸面包
    2020-04-20
    受老师启发,决定慢慢写一个browser,以下是一些好资料,也希望同学们多多补充,一起进步。 SO:https://stackoverflow.com/questions/598841/how-to-get-started-building-a-web-browser Blog: https://limpet.net/mbrubeck/2014/08/08/toy-layout-engine-1.html (Matt开的专栏,教你怎么用Rust写浏览器,Github上有不少项目受此启发。 Github: https://github.com/joegesualdo/abbott (Node.Js) https://github.com/cytle/toy-browser (我们同学的作品) https://github.com/ryanmcdermott/dumbkit (Pure JS) https://github.com/aimergenge/toy-html-parser (专门词法分析的,之前有同学贴过) https://github.com/mbrubeck/robinson (受Matt启发) https://github.com/maekawatoshiki/naglfar (Rust) 慢慢写,总有写完的一天!(笑
    展开
    共 4 条评论
    28
  • Fuhui
    2019-02-21
    浏览器工作流程: 从服务器端获取文档数据☞浏览器解析成DOM树☞CSS根据DOM规则开始排版文档内容☞浏览器分文字和图形进行渲染在窗口展现☞接下来就是JS的交互了。 自己根据印象总结下。
    13
  • wz
    2019-02-19
    是用canvas模拟一个iframe吗

    作者回复: 嗯 差不多是这个意思吧

    8
  • 宅。
    2019-11-05
    为什么我们专业老师说windows重绘机制下,只有窗口遮盖会产生无效矩形,鼠标划过是不会导致重绘的?

    作者回复: 我们学习的时候,需要有个基本的逻辑判断能力,而不是根据“谁说的”来判断对错。 如果鼠标划过不会重绘,那么鼠标遮盖住的内容是怎么出来的呢?

    共 6 条评论
    6
  • 温木
    2019-04-28
    老师有几个问题希望回答: 文中提到:“注意,我们这里讲的渲染过程,是不会把子元素绘制到渲染的位图上的,这样,当父子元素的相对位置发生变化时,可以保证渲染的结果能够最大程度被缓存,减少重新渲染。” 1.缓存的是什么东西,位图吗?如何减少重新渲染的? 2.是不是所有的元素都有对应的位图? 文中你举了两个例子,说如果只有一个位图,任何改变都会引起所有重新渲染。 这个好理解,一个大位图就是一个单元,任何的改动相当于这个单元被变了,所以要重新创建这个,这样理解对吗? 但是你下面又说如果不合并,每次还是所有的重新渲染。 感觉说的很矛盾啊,如果上面说的缓存的位图,那么不合并的位图就不缓存吗,不能减少重新渲染吗?另外如果不合并,有元素变更,所有的都重新进行了渲染,那是不是合并了也会重新渲染呢?只是单元的个数减少从而提升了性能,就像http请求使用长连接,合并小请求,减少简历三次握手的次数是一个道理呢? 希望老师能回答一下我的疑问。
    展开
    5
  • 🐻🔫🐸
    2019-03-07
    所以重绘不一定是什么恐怖的事情,真正的问题在于需要重绘的位图尺寸,不知道可以这么理解吗
    4
  • 宝贝
    2019-02-19
    做一个玩具浏览器的想法有点大胆好玩哈哈基础打好后可以尝试!
    4
  • espelansa
    2019-02-22
    老师您好!在合成这边您写道“如果我们所有的元素都不合成,那么必须要重新绘制所有的元素”,这里我不是特别明白,为什么会需要重新绘制呢?

    作者回复: 你最终把元素画到屏幕上,当然所有元素都要重新绘制了。

    共 4 条评论
    3
  • 陈斌
    2019-03-13
    总结: 页面图片的本质就是一张“完整的二维表格(页面图片的每个像素对应的颜色保存进去的二维表格)” 渲染:把DOM树的各个元素变成位图(二维表格) 合成(可选):把不会经常改变位置和样式的元素的位图“拼接起来”,变成一个较大的合成后的位图(暂时称之为“较大的位图”)。(有个问题:合成只能合成有父子关系的元素的位图吗?) 绘制:把各个DOM元素的位图 和 “较大的位图”“拼接”成一个最终位图,交给操作系统,最后呈现出页面图片。 请问老师,这样总结有什么不妥之处?
    展开
    2
  • 子雅
    2019-03-12
    请问老师伪元素是怎么渲染展示的呢?
    1
  • Mr.Ren
    2019-02-23
    感觉一遍看下来不是太懂,只能多看两遍
    1
  • oillie
    2019-02-21
    跟developers.google上的用词有点不太一样,developers.google上是paint(绘制),对应本文中的render(渲染) 另外合成这块有点出入,以文中的例子来说,我的先前的认知是页面默认只有一个layer(chrome里的GraphicsLayer),c元素有transform样式属性,则单独生成一个layer,而合成是指gpu把两张layer合成一张位图
    展开

    作者回复: 合成的位图不可能就叫位图吧……

    共 2 条评论
    1
  • Hulk
    2019-02-21
    你好,我理解客户端在使用基于webkit的webView控件的时候,‘浏览器’的工作流程也是这样的吧? 大神能否明确一下浏览器在完成哪个节点以后会让webView控件发出webViewLoadFinish的通知? 此前遇到过一下问题,比如H5前端同学所言js是在页面加载成功后立马调用的,而客户端的同学未收到加载成功(收到webViewLoadFinish回调)却也看到了js的执行效果;所以这两端同学所言的加载成功,想必不是一回事?
    展开

    作者回复: 没记错的话LoadFinish应该是跟js里的onload同时发出的。 你可以让你们前端把performance全都打印出来跟你对一下。

    1
  • CC
    2019-02-19
    没想到之前对“重绘”的理解并不准确,发生的频率远比想象的高很多,学到了。
    1
  • 全林聪
    2019-08-10
    如果每个盒子都会绘制一张位图, 父子盒子不合并成一张位图, 那么会占据大量的内存, 在把位图渲染到界面的时候, 又会花费大量的GPU, CPU. 所以怎么去合并位图就等同减少内存占用和减少CPU/GPU耗能了, 至于原文说, "可以保证渲染的结果能够最大程度被缓存", 我想指的是, 比如js改变了某一个元素, 不需要绘制位图->合成位图2步,而是只需要绘制位图这一步就可以了, 而原来存在的位图任然有效, 至于渲染成界面, 原文说, "那么如果我们所有的元素都不合成,会怎样呢?结果就是,相当于每次我们都必须要重新绘制所有的元素", 这里的每次都要必须绘制所有的元素, 我觉得有问题, 如果是第一次绘制, 那么会把所有的元素都绘制一次到UI界面上, 如果用js只修改了一个元素, 那么只影响到一个位图的变化, 那么也只会增量绘制一个位图的内容吧?
    展开

    作者回复: 怎么增量呢,这个位图可能被别人盖住了一半,你怎么算它的增量区域?

  • 白晓宇(7530)
    2019-06-11
    听起来很像设计原型图
  • 忍冬
    2019-05-23
    老师请问下,对这句话不太懂“那么如果我们所有的元素都不合成,会怎样呢?结果就是,相当于每次我们都必须要要重新绘制所有的元素,这也不是对性能有好的选择”,照您说的渲染过程是把每一个元素对应的盒变成位图,既然是这样那我即使都不合成,也不太会影响呀 修改哪个元素就重新渲染下哪个元素呀
    共 1 条评论
  • Geek_eea87d
    2019-04-09
    作者大大,我想知道如果想深层次了解这方面知识应该查看的书籍或者是方向是什么

    作者回复: 书我不知道,可以看看chromium项目的邮件组,自己挑选阅读一些它的源代码。

    1