【进阶第16期】从输入URL到页面呈现发生了什么?—— 浏览器工作原理(上)

176 阅读21分钟

前言

该篇文章是本系列前端进阶之路文章的第六篇,旨在深入探讨浏览器的工作原理, 先了解下浏览器历史,再通过以chrome浏览器组成以及它的多进程架构角度来剖析浏览器内部是如何来完成从输入URL到页面呈现这个过程的。

浏览器前世今生

  互联网的浪潮从未停息,而浏览器这个用以网上冲浪的冲浪板也一直在变得愈加精良。自人们进入互联网时代以来,即已经发生了三次浏览器大战。 1、第一次浏览器大战的主角是IE和Netscape,最终IE凭借着Windows的庞大身躯推倒了Netscape; 2、第二次浏览器大战Netscape浴火重生化身为火狐狸Firefox,一口咬了IE身上一大块肥肉;正在Firefox和IE正缠绵不息之时,突然凭空杀出个Chrome——这名出身豪门Google的小伙子一下子成长得额外精壮,上串势头凶猛,追得两位前辈娇喘吁吁。这位Chrome究竟是何方人物,能练就如此神功,在短短几年内就成为互联网浏览的一大主流,市场份额赶超了Firefox不说,甚至还曾在短时间内压过了微软帝国的IE,形成天下三分的第三次浏览器大战的格局? 3、第三次浏览器大战随着移动互联网的兴起,浏览器大战格局就更加混乱了,这里不做具体展开。

目前,主流浏览器有五种——IE、Firefox、Safari、Chrome及Opera

八卦一下chrome的血统

  出于好奇,不少人都八卦了一下Chrome的来历,然后发现Chrome的背后深藏着Webkit这个名字。对浏览器有所研究的朋友,应该也会或多或少地闻过Webkit的大名。Webkit源于KDE开源项目,兴盛于苹果公司的Safari项目,它身上有诸多创新,近年来风行的HTML5以及CSS3潮流都和Webkit脱不开关系。Webkit小巧、灵活但又十分强大,而且源代码开放,深得业界喜爱。从诺基亚S60上的浏览器,到价比肾贵的iPhone上的Safari,我们都能看到Webkit的身影。

  从来都不是嗅觉迟钝的公司,Webkit的优秀自然也吸引了这位互联网枭雄的眼光。2008年9月,Google发布了Chrome的测试版,Chrome面世了。Chrome使用了Webkit的代码,继承了Webkit的优良排版引擎,渲染页面速度惊人。既然Chrome使用了Webkit的源代码,也使用了Webkit的排版引擎,那么我们是否就可以认为,Google只是在Webkit上面加了一层壳就做出了Chrome呢?

webkit VS chromium VS chrome

项目名由来区别实质
webkitWebkit源于KDE的开源项目KHTML,兴盛于苹果公司的Safari项目由两部分组成,一部分是WebCore排版引擎(由苹果公司开发),用以解析HTML语言和CSS框架;另一部分为JSCore 解析引擎,用以执行网页JS脚本开源项目,浏览器内核,渲染引擎
chromium早期建立在webkit之上的开源项目,后面由于webkit2与自身的沙箱设计冲突,由google发起Blink 是chromium 的一个分支(与webkit理念出现分歧)作为新的渲染引擎替代方案1、Google对Webkit的代码重新梳理,Chromium代码的可读性和编译效率远比Webkit高,对比Chromium的代码,Webkit的代码堪比天书,开发难度高得多。2、使用V8引擎,V8引擎比Webkit的JSCore效率更高实验项目、开源软件
chrome早期Chrome只是继承了Webkit的WebCore部分,在JS引擎上使用了Google引以为豪的V8引擎,大大地提高了脚本执行速度,这也是为什么Chrome会如此快的重要原因。后来使用自己发起的开源项目chromium替代商业项目

浏览器的组成

组成部分描述
用户界面包括地址栏、后退/前进按钮、书签目录等
浏览器引擎用来查询及操作渲染引擎的接口
渲染引擎用来显示请求的内容,例如,如果请求内容为html,它负责解析html及css,并将解析后的结果显示出来。
网络用来完成网络调用,例如http请求,它具有平台无关的接口,可以在不同平台上工作
JS解释器用来解释执行JS代码
数据存储属于持久层,浏览器需要在硬盘中保存类似cookie的各种数据,HTML5定义了web database技术,这是一种轻量级完整的客户端存储技术

渲染引擎

浏览器的渲染引擎,也称为排版引擎,浏览器内核。

浏览器内核浏览器代表
Trident代表IE(windows)
Presto代表以前Opera,目前使用Blink
Gecko代表Firefox
webkit代表Safari
Blink代表chromium,其中webcore排版引擎的一个分支,使用V8引擎

浏览器的多进程架构(以chrome为例)

对一些前端开发同学来说,进程和线程的概念可能会有些模糊,为了更好的理解浏览器的多进程架构,这里我们简单讨论一下进程和线程。

进程(process)和线程(thread)概念

概念:线程是独立调度和分派的基本单位,也是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
详细解释就是,启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程。多线程可以并行处理任务,但是线程是不能单独存在的,它是由进程来启动和管理的。而进程中使用多线程并行处理能提升运算效率,一个进程就是一个程序的运行实例。

进程VS线程关系

模块描述
崩溃进程中的任意一线程执行出错,都会导致整个进程的崩溃
共享数据一个进程中的多个线程可以读写进程的公共数据
回收内存当一个进程关闭,操作系统会回收进程的所占内存
进程隔离进程之间的内容是相互隔离的,每一个进程不能访问其他进程的数据

早期单进程架构时代

浏览器的所有功能模块都是运行在同一个进程里(包括网络、插件、JavaScript运行环境、渲染引擎和页面等),它包含以下两个线程

  • 页面主线程:页面渲染、页面展示、javascript环境、插件模块
  • 网络线程:负责资源请求

如此多的功能模块运行在一个进程里,是导致单进程浏览器不稳定、不流畅和不安全的一个主要因素

  • 不稳定。插件是最容易出问题的模块,一个插件的意外崩溃会引起整个浏览器的崩溃,渲染引擎的崩溃也会导致整个浏览器的崩溃,一些复杂的 JavaScript 代码就有可能引起渲染引擎模块的崩溃
  • 不流畅。渲染模块、JavaScript 执行环境以及插件都是运行在同一个线程中,意味着同一时刻只能有一个模块可以执行;另外,运行一个复杂点的页面再关闭页面,会存在内存不能完全回收的情况,页面的内存泄漏也是单进程变慢的一个重要原因
  • 不安全。插件可以使用 C/C++ 等代码编写,通过插件可以获取到操作系统的任意资源,至于页面脚本,它可以通过浏览器的漏洞来获取系统权限

优点:页面渲染互不影响,安全性,沙箱,健壮性,站点隔离(每个Iframe开启一个独立渲染进程)

Chrome 多进程架构(有哪些进程?这些进程都负责什么?)

Chrome 采用的多进程架构,其顶层存在一个 Browser process 用以协调浏览器的其它进程。最新的 Chrome 浏览器包括:1个浏览器(Browser)主进程、1 个GPU进程、1 个网络(NetWork)进程、多个渲染进程和多个插件进程

进程职能
浏览器进程控制除标签页外的所有用户界面,包括地址栏、前进/后退等,以及浏览器其他进程间协调工作。
网络进程主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,后来才独立出来,成为一个单独的进程
渲染进程控制tab标签内显示的所有内容,核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下
GPU进程负责整个浏览器界面的渲染。其实,Chrome 刚开始发布的时候是没有 GPU 进程的。而 GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了GPU 进程
插件进程主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响
utility进程有时候浏览器主进程需要做一些“危险”的事情,比如图片解码、文件解压缩。如果这些“危险”的操作发生了失败,会导致整个主进程发生异常崩溃,这是我们不愿意看到的。因此Chromium设计出了一个utility进程的机制。主进程临时需要做一些不方便的任务的情况下,可以启动一个utility进程来代替主进程执行,主进程与utility进程之间通过IPC消息来通信。

如果当前在资源充足的环境下还会有如下其他进程

  • UI进程
  • 存储进程
  • 设备进程
  • Audio进程
  • Video进程
  • Profile进程 ...

从输入URL到页面呈现过程中,这些进程是如何相互配合完成一个页面的渲染的?

导航过程发生了什么?

也许大多数人使用 Chrome 最多的场景就是在地址栏输入关键字进行搜索或者输入地址导航到某个网站,我们来看看浏览器是怎么看待这个过程的。我们知道浏览器 Tab 外的工作主要由 Browser Process 掌控,Browser Process 又对这些工作进一步划分,使用不同线程进行处理:

  • UI thread: 控制浏览器上的按钮及输入框
  • network thread: 处理网络请求,从网上获取数据
  • storage thread: 控制文件等的访问

当我们在浏览器地址栏中输入文字,并点击回车获得页面内容的过程在浏览器看来可以分为以下几步:

1. 处理输入

  • 浏览器进程的UI thread 首先需要判断用户输入的是 URL 还是 query;

2. 开始导航

  • 当用户点击回车键后,浏览器进程的UI thread 通知 network thread 获取网页内容,并控制 tab 上的 spinner 展现,表示正在加载中。
  • network thread 会执行 DNS 查询,随后为请求建立 TLS 连接
  • 如果 network thread 接收到了重定向请求头如 301,network thread 会通知 UI thread 服务器要求重定向,之后,另外一个 URL 请求会被触发。

3. 读取响应

  • 当请求响应返回的时候,network thread 会依据HTTP响应头的Content-TypeMIME Type字段判断响应内容的格式
  • 如果响应内容的格式是 HTML ,下一步将会把这些数据传递给renderer process,如果是 zip 文件或者其它文件,会把相关数据传输给下载管理器。
  • Safe Browsing 检查也会在此时触发,如果域名或者请求内容匹配到已知的恶意站点,network thread 会展示一个警告页。此外 CORB 检测也会触发确保敏感数据不会被传递给渲染进程。

4. 查找渲染进程

当上述所有检查完成,network thread 确信浏览器可以导航到请求网页,network thread 会通知 UI thread 数据已经准备好,UI thread 会查找到一个 renderer process 进行网页的渲染。

由于网络请求获取响应需要时间,这里其实还存在着一个加速方案。当 UI thread 发送 URL 请求给 network thread 时,浏览器其实已经知道了将要导航到那个站点。UI thread 会并行的预先查找和启动一个渲染进程,如果一切正常,当 network thread 接收到数据时,渲染进程已经准备就绪了,但是如果遇到重定向,准备好的渲染进程也许就不可用了,这时候就需要重启一个新的渲染进程。

5. 确认导航

经过了上述过程,数据以及渲染进程都可用了, Browser Process 会给 renderer process 发送 IPC 消息来确认导航,一旦 Browser Process 收到 renderer process 的渲染确认消息,导航过程结束,页面加载过程开始。

此时,地址栏会更新,展示出新页面的网页信息。history tab会更新,可通过返回键返回导航来的页面,为了让关闭 tab 或者窗口后便于恢复,这些信息会存放在硬盘中。

6. 额外的步骤

渲染进程如何处理网络资源?

渲染进程几乎负责 Tab 内的所有事情,渲染进程的核心目的在于转换 HTML CSS JS 为用户可交互的 web 页面。渲染进程中主要包含以下线程:

  • 主线程 Main thread
  • 工作线程 Worker thread
  • 排版线程 Compositor thread
  • 光栅线程 Raster thread

渲染进程包含的线程

1. 主线程解析HTML并构建DOM Tree

当渲染进程接收到导航的确认信息,开始接受 HTML 数据时,主线程会解析文本字符串并构建DOM Tree。具体解析生成DOM Tree过程这里就不在赘述了,上篇文章已经讲解的很清楚了。

主线程解析HTML并构建DOM Tree

2. 加载次级的资源(JS/CSS)

网页中常常包含诸如图片,CSS,JS 等额外的资源,这些资源需要从网络上或者 cache 中获取。主进程可以在构建 DOM 的过程中会逐一请求它们,为了加速 preload scanner 会同时运行,如果在html中存在 <img> <link> 等标签,preload scanner 会把这些请求传递给 Browser process 中的 network thread 进行相关资源的下载

  • JavaScript can block the parsing

3. JS 的下载与执行

当遇到 <script> 标签时,渲染进程会停止解析 HTML,而去加载,解析和执行 JS 代码,停止解析 html 的原因在于 JS 可能会改变 DOM 的结构(使用诸如 document.write()等API)。

不过开发者其实也有多种方式来告知浏览器应对如何应对某个资源,比如说如果在<script> 标签上添加了 asyncdefer 等属性,浏览器会异步的加载和执行JS代码,而不会阻塞渲染。更多的方法可参考 Resource Prioritization – Getting the Browser to Help You

4. 样式计算(Style)

仅仅渲染 DOM 还不足以获知页面的具体样式,主进程还会基于 CSS 选择器解析 CSS 获取每一个节点的最终的计算样式值。即使页面不加载任何CSS,浏览器对每个元素也会有一个默认的样式。

渲染进程的主线程计算每一个元素节点的最终样式值

5. 获取布局(Layout)

当渲染对象被创建并添加到树中,它们并没有位置和大小,计算这些值的过程称为layout或reflow布局是一个递归的过程,由根渲染对象开始,它对应html文档元素,布局继续递归的通过一些或所有的frame层级,为每个需要几何信息的渲染对象进行计算。 根渲染对象的位置是0,0,它的大小是viewport-浏览器窗口的可见部分。 所有的渲染对象都有一个layout或reflow方法,每个渲染对象调用需要布局的children的layout方法。布局其实是找到所有元素的几何关系的过程。其具体过程如下:

通过遍历 DOM 及相关元素的计算样式,主线程会构建出包含每个元素的坐标信息及盒子大小的布局树。Layout Tree和 DOM Tree类似,但是其中只包含页面可见的元素,如果一个元素设置了 display:none ,这个元素不会出现在布局树上,伪元素(如 :before 或 :after)虽然在 DOM 树上不可见,但是在布局树上是可见的。

6. 绘制各元素(Paint)

即使知道了不同元素的位置及样式信息(大小、形状),我们还需要知道不同元素的绘制先后顺序才能正确绘制出整个页面。在绘制阶段,主线程会遍历布局树以创建绘制记录。绘制记录可以看做是记录各元素绘制先后顺序的笔记。如果你使用过javascript在canvas绘制图形,你就能清晰理解这个过程(

  • 先移动到(x1,y1)点,画矩形A
  • 填充背景,插入文本
  • 再移动到(x2,y2),画圆B,
  • 再插入文本等等

7. 合成帧(Composite)

  • 分层

一旦Layer Tree被并且绘制顺序确定之后,主线程就会将信息提交给合成线程。然后合成线程光栅化每个Layer对象。由于一个图层可能跟页面一样大,合成器线程将它们切分为许多图块title,然后发给栅格线程。栅格线程栅格化每个title并将它们存储在GPU内存中

  • 划分块

  • 使用光栅线程和合成线程

重绘和重排

注意:在整个页面的生命周期中,网页生成的时候,至少会渲染(上述的1-7步骤)一次,在用户访问的过程中,可能还会不断触发重排(reflow)和重绘(repaint),不管页面发生了重绘还是重排,都会影响性能,最可怕的是重排,会使我们付出高额的性能代价。下面简单介绍下这两个概念:

  • 重绘(repaint):当一个元素的外观发生改变(背景颜色,边框等),但没有改变布局,重新把元素外观绘制出来的过程,叫做重绘。
  • 重排(reflow): 当DOM的变化影响了元素的几何信息(元素的的位置和尺寸大小),浏览器需要重新计算元素的几何属性,将其安放在界面中的正确位置,这个过程叫做重排。 需要注意都是:重绘不一定导致重排,但重排一定会导致重绘

三种常见的渲染流程

1. JS/CSS>计算样式>布局>绘制>渲染层合并


这张图上渲染流程对应的是reflow渲染的过程,它会经过布局再绘制。

举个例子:

<div id="box1" style="width:100px;height:100px;background:black;">box1</div>
<div id="box2" style="width:100px;height:100px;background:green;">box2</div>

$(document).keydown(function(e){
    if(e.altKey){
            $("#box1").css({position:"absolute","right":"200px"})// 既有重绘也有回流
    }
})

为了方便查找通过键盘事件来标记,当我们按下alt键时,移动box1,借助chrome performance 记录这一个过程截图如下: image.png

通过EventLog查看到了这一个动作触发了后续一系列的流程:

Recalculate Style > Layout > Update layer Tree > Paint > Composite Layers

注意:中间有两个Hit test update LayerTree 具体什么原因 造成的,我也有点懵,知道同学可以回复下

2. JS/CSS>计算样式>绘制>渲染层合并


这张图上渲染流程对应的是repaint渲染的过程,它不需要经过布局,只需要绘制当前的元素,不需要重新计算它的父元素。

举个类似例子:

<div id="box1" style="width:100px;height:100px;background:black;">box1</div>
<div id="box2" style="width:100px;height:100px;background:green;">box2</div>

$(document).keydown(function(e){
    if(e.altKey){
        $("#box1").css({background:"red"})// 既有重绘也有回流
    }
})

同样:为了方便查找通过键盘事件来标记,当我们按下alt键时,更改box1北京颜色,借助chrome performance 记录这一个过程截图如下:

image.png

Recalculate Style > Layout(没执行) > Update layer Tree > Paint > Composite Layers

3. JS/CSS>计算样式>渲染层合并

这张图上渲染流程比较特殊,它不选经过布局、绘制,它只需要在合成层上修改。

跑了几个demo ,还是没有实现想要的结果,知道的同学可以帮忙补上(太懒,太笨)^^ 哈哈。

什么时候会发生回流呢?

  • 1、添加或删除可见的DOM元素
  • 2、元素的位置发生变化
  • 3、元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)
  • 4、内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代。
  • 5、页面一开始渲染的时候(这肯定避免不了)
  • 6、浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)

而重绘是指在布局和几何大小都不变得情况下,比如次改一下background-color,或者改动一下字体颜色的color等。

注意:回流一定会触发重绘,而重绘不一定会回流

如何减少回流和重绘

1、CSS优化法

  • 1、使用 transform 替代 top

  • 2、使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局

  • 3、避免使用table布局,可能很小的一个小改动会造成整个 table 的重新布局。

  • 4、尽可能在DOM树的最末端改变class,回流是不可避免的,但可以减少其影响。尽可能在DOM树的最末端改变class,可以限制了回流的范围,使其影响尽可能少的节点。

  • 5、避免设置多层内联样式,CSS 选择符从右往左匹配查找,避免节点层级过多。

  • 6、将动画效果应用到position属性为absolute或fixed的元素上,避免影响其他元素的布局,这样只是一个重绘,而不是回流,同时,控制动画速度可以选择 requestAnimationFrame,详见探讨 requestAnimationFrame。

  • 7、避免使用CSS表达式,可能会引发回流。

  • 8、将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点,例如will-change、video、iframe等标签,浏览器会自动将该节点变为图层。

  • 9、CSS3 硬件加速(GPU加速),使用css3硬件加速,可以让transform、opacity、filters这些动画不会引起回流重绘 。但是对于动画的其它属性,比如background-color这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。

2、JavaScript优化法

  • 1 避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性。
  • 2 避免频繁操作DOM,创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中。 
  • 3 避免频繁读取样式会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来

拓展