creeperyang / blog

前端博客,关注基础知识和性能优化。
MIT License
2.63k stars 211 forks source link

浏览器的工作原理 #46

Closed creeperyang closed 1 year ago

creeperyang commented 6 years ago

这是浏览器的工作原理:新式网络浏览器幕后揭秘(英文)/(中文)的阅读笔记。尽量精简原文章,方便回忆和复习相关概念。

十分推荐阅读原文(原文应该非常有名),这里只是自用的笔记和归纳~

浏览器的工作原理

一、浏览器的结构

浏览器的主要组件为:

二、渲染引擎

渲染引擎负责渲染——即渲染HTML/XML文档或者图片(通过插件可以渲染PDF等等)。渲染引擎有

(一)渲染主流程

浏览器从网络层获取请求的文档内容,然后开始渲染流程:

注意,渲染过程是渐进式的。浏览器会尽早展示文档内容,即不会在所有HTML文档解析完成后才会去构建render tree,而是部分内容被解析和展示,并继续解析和展示剩下的。

对chrome而言,渲染的具体流程是

对firefox而言,

(二)处理脚本和样式表的顺序

  1. script 是同步的

    web模型一直是同步的,即网页作者希望引擎遇到<script>标签时可以立即解析并执行——停止解析HTML,执行脚本(如果是外部脚本,先下载)。可以用defer属性指定脚本是异步的——不会停止文档解析,在文档解析完成后执行。

  2. Speculative parsing(预解析)

    当执行脚本时,其它线程会解析剩下的文档,找出里面的外部资源(script/style/img)来提前加载(可以并行加载)。这种解析只是去查找需要加载的外部资源,不会修改content tree

    所以我们可以看到多个外部资源并行下载。

  3. 样式

    样式表有不同的模型。理论上,样式表不会更改 DOM tree,似乎没有必要等待样式表并停止文档解析。但有个问题,如果在文档解析阶段,脚本访问样式信息怎么办?Firefox会在脚本加载和解析阶段禁止所有的脚本;对于 WebKit 而言,仅当脚本尝试访问的样式属性可能受尚未加载的样式表影响时,它才会禁止该脚本。

这就是为什么推荐样式放在<head>里而脚本放在<body>底部。

(三)Render tree construction

构建 DOM tree的同时,浏览器还会构建另一个树:渲染树(render tree)。这是由可视化元素按照其显示顺序而组成的树,也是文档的可视化表示。它的作用是保证按照正确的顺序来绘制内容。

渲染树的每个节点(renderer)代表一个矩形区域——对应DOM元素的CSS Box。

renderer 和 DOM元素对应,但非一一对应。比如display:none的元素没有对应的renderer;比如select对应3个renderer(display area/drop down list box /button)。另外,根据css spec,一个inline元素只能包含一个block元素或者多个inline元素,如果不符规则,就会创建anonymous block renderer。

有些 renderers 与对应的 DOM 节点,在各自树中的位置不同。比如浮动定位和绝对定位的元素,它们在normal flow之外,放置在树的其它地方,并映射到真正的renderer,而放在原位的是placeholder renderer。

渐进式处理

WebKit 使用一个标记来表示是否所有的顶级样式表(包括 @imports)均已加载完毕。如果在attaching(DOM+CSSOM --> Render tree)过程中样式尚未完全加载,则使用占位符,并在文档中进行标注,等样式表加载完毕后再重新计算。

(四)Layout

renderer在创建完成并添加到render tree时,并不包含 位置和大小 信息。计算这些值的过程称为布局或重排(Layout/Reflow)。

HTML 采用基于流的布局模型,这意味着大多数情况下只要一次遍历就能计算出几何信息。处于流中靠后位置元素通常不会影响靠前位置元素的几何特征,因此布局可以按从左至右、从上至下的顺序遍历文档。

Dirty 位系统

为避免对所有细小更改都进行整体布局,浏览器采用了一种“dirty 位”系统。如果renderer有更改,或者其自身及其children被标注为“dirty”——则需要进行布局。

有两种标记:“dirty”和“children are dirty”。“children are dirty”表示renderer自身没有变化,但它的children需要布局。

全局布局和增量布局

全局布局是指触发了整个render tree的布局,触发原因可能包括:

布局可以采用增量方式,也就是只对 dirty 的 renderer 进行布局(这样可能存在需要进行额外布局的弊端)。

当renderer为 dirty 时,触发增量布局(异步)。例如,当来自网络的额外内容添加到 DOM 树之后,新的renderer附加到了render tree中。

异步布局和同步布局

优化

布局处理

布局过程通常如下:

宽度计算

renderer宽度是根据容器块(container block)的宽度、renderer样式中的“width”属性以及边距和边框计算得出的。

换行

如果renderer在布局过程中需要换行,会立即停止布局,并告知其父renderer需要换行。父renderer会创建额外的renderer,并对其调用布局。

(五)Painting

在绘制阶段,会遍历render tree,并调用renderer的“paint”方法,将renderer的内容显示在屏幕上。绘制工作是使用用户界面基础组件(UI infrastructure component)完成的。

全局绘制和增量绘制

和布局一样,绘制也分为全局(绘制整个render tree)和增量两种。在增量绘制中,部分renderer发生了更改,但是不会影响整个树。更改后的renderer将其在屏幕上对应的矩形区域设为无效,这导致 OS 将其视为一块“dirty 区域”,并生成“paint”事件。OS 会很巧妙地将多个区域合并成一个。

绘制顺序

CSS2 defines the order of the painting process. This is actually the order in which the elements are stacked in the stacking contexts. This order affects painting since the stacks are painted from back to front.

block renderer的堆栈顺序是:

  1. 背景颜色
  2. 背景图片
  3. 边框
  4. children
  5. 轮廓(outline)

动态变化

在发生变化时,浏览器会尽可能做出最小的响应。比如元素的颜色改变后,只会对该元素进行重绘。元素的位置改变后,只会对该元素及其子元素(可能还有同级元素)进行布局和重绘。添加 DOM 节点后,会对该节点进行布局和重绘。

一些重大变化(例如增大“html”元素的字体)会导致缓存无效,使得整个render tree都会进行重新布局和绘制。

结合整个render tree构建和lauout,paint阶段,可以去思考怎么减少relayout/repaint。

渲染引擎的线程(The rendering engine's threads)

渲染引擎是单线程的。几乎所有操作(除了网络操作)都是在单线程中进行的。在 Firefox 和 Safari 中,该线程就是浏览器的主线程。而在 Chrome 浏览器中,该线程是tab进程的主线程。

网络操作可由多个线程并行执行。并行连接数是有限的(通常为 2~6 个)。

Event loop

The browser main thread is an event loop. It's an infinite loop that keeps the process alive. It waits for events (like layout and paint events) and processes them.

这里可配合 #21 阅读,结合上面一小段,可展开讨论下。

在浏览器的具体实现里,浏览器内核(渲染进程)是多线程的。其中最重要的线程有(Blink 为例):

~GUI线程和JS线程是互斥的(因为JavaScript可操纵DOM)。这就是为什么JS长时间运行会导致浏览器失去响应。~ 具体信息可参考 @starliiit 评论(和相关链接)。

starliiit commented 5 years ago

个人感觉最后一段引用里提到的「GUI 线程和 JS 线程互斥」的说法好像不太对。DOM tree/render tree 的构建和 JS 引擎应该是在同一个线程里执行的。

How Blink Works 里面提到:

How many threads are created in a renderer process?

Blink has one main thread, N worker threads and a couple of internal threads.

Almost all important things happen on the main thread. All JavaScript (except workers), DOM, CSS, style and layout calculations run on the main thread.

Inside look at modern web browser(part 3) 里面也提到,layer tree 构建是在主线程里执行的,执行完毕之后由 composite 线程和 raster 线程处理 layer tree 去完成光栅化,实际生成图像的步骤。

creeperyang commented 5 years ago

@starliiit 感谢提出。