今天要写的是一篇关于浏览器底层工作原理的技术科普文,作为一个前端爱好者,在平时的日常工作中,最常打交道的莫过于浏览器了。不过,大多数人并不知道,在我们打开浏览器并浏览一个网页时,浏览器底层具体进行了什么工作。今天我将带领大家,逐步剖析我们在使用浏览器访问网页的过程中,浏览器究竟为我们做了些什么。
在进入正文之前,先给大家引入一个话题。相信每个人都曾迷惑于互联网一些复杂难懂的概念,什么区块链阿,O2C阿,互联网思维阿….. 不少人似乎被这些模糊的概念吓住了,怀疑自己是不是跟不上当今的互联网时代了。其实,当你仔细去了解过后,你会发现这些所谓的互联网新名词,讲的也不过是一些平常的东西。只是这些通俗易懂的道理,被人用高大上的词汇包装起来而已。为了证明一下,我特地去网上找了这张图片:
怎么样?是不是感觉自己一下子茅舍顿开、豁然开朗。其实,在互联网技术领域,也有非常多高大上的专业术语,但当你仔细去剖析这些知识点后,你会发现其实道理也不过如此。给大家讲这些,主要是为了消除大家对于互联网专业术语的恐惧,因为究其本质,这些都是我们可以理解的理论。
现在开始进入正文,即然谈到了浏览器,在这里不得不先向大家介绍一下现在市面上的主流浏览器。所谓的主流浏览器,有两个评判标准:1.有独立研发的内核 2.占有一定的市场份额。内核是浏览器最重要的部分,是浏览器的核心,也称为“渲染引擎(Rendering Engine)”。它用来解释网页语法,把内部的代码转化为用户可见的视图。浏览器内核决定了浏览器该如何显示网页内容以及页面的格式信息,不同的浏览器内核的兼容性不同,其渲染效果也各有所异。
目前市面上主要有五大主流浏览器,分别是IE、火狐Firefox、谷歌Chrome、苹果Safari 和 欧朋Opera。它们所用的内核分别是Trident内核、Gecko内核、Blink内核、Webkit内核 和 Blink内核。
浏览器 | 内核 |
IE浏览器 | Trident内核,也是俗称的IE内核 |
Chrome浏览器 | 以前是Webkit内核,现在是Blink内核,统称为Chrome内核或Chromium内核 |
Firefox浏览器 | Gecko内核,俗称Firefox内核 |
Safari浏览器 | Webkit内核 |
Opera浏览器 | 最初是自己的Presto内核,后来加入谷歌,从Webkit又到了Blink内核 |
360浏览器、猎豹浏览器 | IE+Chrome双内核 |
搜狗、遨游、QQ浏览器 | Trident(兼容模式)+Webkit(高速模式) |
百度浏览器、世界之窗 | IE内核 |
2345浏览器 | IE+Chrome双内核 |
对于前端开发人员来说,要考虑到许多用户体验的问题,开发的项目必须兼容各大主流浏览器,因此必须要先了解各大浏览器的内核。内核研发的时间成本和技术成本都很高,我们国内目前还没有独立研发的浏览器内核,国产的浏览器,如百度浏览器、QQ浏览器、360浏览器等,都是基于国外的内核技术。就连去年引起一时轰动的首个“自主研发”的国产浏览器——红芯浏览器,也在后来承认是抄袭了谷歌浏览器的内核。
介绍完主流浏览器,就来讲解浏览器的工作过程。浏览器的工作过程主要分为两个部分:网络通信部分和页面渲染部分。
浏览器工作的第一部分:网络通信。
在我们打开一个浏览器浏览一个网站时,我们首先需要在地址栏输入网站的网址,通常我们输入的都是网站名 ( 如www.baidu.com ) 。
一个完整的网址叫作统一资源定位符URL,即 http://www.baidu.com的格式 ,URL由协议名( 如http ) 和服务器名 ( 如www ) 与域名 ( 如baidu.com ) 连在一起的网站名( www.baidu.com )构成。我们也可以直接在地址栏输入网站的ip地址,如联通运营商的百度ip地址为163.177.151.109 。
当我们输入域名并按下回车键后,DNS (Domain Name System) 就会为我们解析域名(DNS也叫域名系统,是万维网上作为域名和IP地址相互映射的一个分布式数据库,能够使用户更方便的访问互联网,而不用去记住那一长串能够被机器直接读取的IP数串)。DNS最终通过我们输入网址的主机名/服务器名 (如www.baidu.com这个网址的主机名就是www) 找到该网址唯一对应的IP地址 (这个过程叫做域名解析或主机名解析)。
在DNS进行域名解析之前,浏览器首先还会进行一系列复杂的工作。它会先搜索自身的DNS缓存,如果找不到自身的缓存,就会开始搜索操作系统的DNS缓存,如果找到了缓存并且缓存没有过期就停止搜索和解析。如果操作系统也找不到缓存,就会尝试读取位于C:\Windows\System32\drivers\etc目录下的hosts文件,查看文件里面有没有该域名对应的IP地址,如果有则解析成功。
如果hosts文件还找不到对应的条目,浏览器会向本地配置首选的运营商DNS服务器发起域名解析请求(该过程为递归请求)。如果运营商的DNS服务器也搜索不到自身所对应的DNS缓存,就会代我们的浏览器向根域的DNS发起请求(该过程为迭代请求)。根域首先会替我们解析顶级域名(.com),返回顶级域名.com的ip地址。运营商的DNS接着向顶级域名.com的ip地址发起请求,顶级域名.com再返回baidu.com域名的ip地址。于是运行商DNS服务器又会向baidu.com域名的ip地址发起请求查找www.baidu.com的ip地址,这时baidu.com域名的DNS服务器通过主机名www找到www.baidu.com的ip地址并返回给运营商的DNS服务器。运营商的DNS服务器接着 (把老子累死对你们有什么好处?) 把最终的ip地址返回给我们的Windows操作系统内核,操作系统内核又把结果返回给浏览器,于是到此浏览器终于得到了www.baidu.com这个网站名的ip地址,解析成功。
解析完URL获得对应的ip地址后,浏览器会遵循超文本传输协议http的规范(现在更多采用https协议,因为https更加安全可靠),向目标网站服务器发起请求。浏览器根据TCP/IP网络协议,利用我们所拿到的ip地址与目标网站服务器建立通信。
关于TCP协议,这里有必要仔细讲解一下。不管你是走前端还是后端,TCP协议是在多数互联网公司面试过程中逃不开的考查点。TCP是一种面向连接的传输层控制协议,在发送数据前,通信双方必须在彼此间建立一条连接,客户端和服务器端的内存里分别保存一份关于对方的信息,如ip地址、端口号等。 TCP可以看成是一种字节流,该协议提供了一种可靠的面向连接服务,采用三次握手建立一个连接,采用四次挥手来关闭一个连接。
三次握手的目的,是为了确认两台主机都具备接收和发送信息的能力,以防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。先通俗的跟大家解释一下所谓的TCP三次握手。
场景假设:
在一次秘密任务的执行过程中,水蜜桃不幸被敌方逮捕关押起来,于是上头派出了哈密瓜执行对水蜜桃的营救行动。哈密瓜潜入敌方关押水蜜桃的房间水管通道口,但是管道太窄了,哈密瓜爬不进去,不知道房间里水蜜桃现在的情况,只能通过折纸船的方式让信息流向水蜜桃房间的下水道,以便和水蜜桃进行通信。
此时的哈密瓜和水蜜桃就相当于是两台要进行通信的主机A和B,但是两台主机要进行通信,要先证明两台主机都有接收和发送信息的能力。
哈密瓜首先要确定房间里的水蜜桃是不是可以与之自由通信,于是他在纸条上写道“呼叫水蜜桃,街头暗号?(seq249)”,将笔和纸船流向水蜜桃的房间,此时哈密瓜还不确定水蜜桃能不能收到他的信息。
我们假设哈密瓜是客户端主机A,水蜜桃是服务器主机B,A需要确认B既可以收信息也可以发信息,B也要确认A既可以收信息也可以发信息之后,才会开始真正的通信。因为如果有任何一方的收或发存在问题,通信都无法成功。这第一张纸条,表示TCP三次握手的第一次握手,主要传递两个信息,一是请求建立连接,二是发出一个序列号。在实际连接中,请求建立连接用SYN=1表示,序列号用seq=n表示,其中n为一个数。第一次握手让服务器主机B知道客户端主机A可以发出消息。
过了一会儿,水蜜桃突然看到了下水道的纸条,知道同伴来救自己,水蜜桃也知道了同伴哈密瓜能给他自由写信,便拿起笔写道:“水蜜桃收到(ack 250),买了否冷?(seq99)”,然后将笔和纸条流回去,此时的水蜜桃并不知道哈密瓜能否收到他的回信。2分钟后,哈密瓜收到了水蜜桃的纸条,知道水蜜桃此时即能自由的收信,也能自由的回信。
这是第二张纸条,也就是第二次握手。我们要注意到哈密瓜发的序号是seq249,水蜜桃回复的是收到ack250。在实际情况中,这次回复一共有三条信息,一是同意建立连接(SYN = 1),二是确认收到了刚才的信息(ack = 刚才的seq + 1),三是发出自己的序列号(seq = x,其中x为一个数)。
我们仔细回想一下,在第一次传条中服务器主机B知道了客户端主机A可以发送信息,第二张纸条发出,客户端主机A接到之后,知道了服务器主机B可以接收也能发出。那么这个时候,还有一个没有证明,那就是服务器主机B还不知道客户端主机A能不能接收到自己的纸条。虽然客户端主机A接收到了纸条,但是服务器主机B并不知道。所以服务器主机B要生成一个数,让客户端主机A回复这个数加一才能确认客户端主机A也能接收到消息。这就是服务器主机B要发出序号的原因。第二次握手让客户端主机A知道了服务器主机B能接收到信息,也能发出信息。
哈密瓜于是继续写道“否冷?why?哈哈哈哈…(ack100)水蜜桃我来救你啦,想不到你也有今天啊哈哈哈(seq 250)”,将纸条流了回去,此时水蜜桃还在等待,水蜜桃并不确定哈密瓜能否正常收到自己的纸条。又过了一会,水蜜桃看到了流过来的纸条,知道哈密瓜看到了他的回信,即知道了哈密瓜也能自由的收信。
这是第三张纸条,也就是第三次握手。这次客户端主机A回复也有三条信息,一是表示现在开始发送(SYN = 0),二是成功收到了服务器主机B的信息(ack=刚才的seq + 1),三是这张纸条的序号(seq=最开始发出的序号 + 1)。第三次握手让B知道了A能接收到。
通过这个例子,相信大家应该可以理解下面的图示了,这就是TCP三次握手的本质。
三次握手之后,他们之间的通信正式建立,于是终于可以安心的给彼此发小纸条了 :
“ 哈密瓜你要怎么救我阿…. 这里好黑风好大,我好空虚好害怕 ”
”你先把品如的衣服脱了 ”
……..
通过TCP三次握手建立了TCP/IP连接后,浏览器会向服务器主机发起一个HTTP-GET方法报文请求。请求中包含访问的URL,也就是 http://www.baidu.com/ ,还有浏览器操作系统信息、编码等。
(关于Cookies:如果Cookies是首次访问,会提示服务器建立用户缓存信息。如果浏览器已存储了该域名下的Cookies,那么浏览器会把Cookies放入HTTP请求头里发给服务器。可以利用Cookies对应键值,找到相应缓存,缓存里面存放着用户名、密码和一些用户设置项。)
服务器端接收到http请求以后,便会响应和处理该http请求,处理之后给浏览器返回html文件。
以上便是浏览器访问网站时网络通信部分的大致过程,至于更为详细的网络通信内部过程,感兴趣的人可以查阅相关网络资料,在此便不再叙述。
接下来为大家介绍浏览器工作的第二部分:页面渲染。
当浏览器拿到服务器返回的HTML文档时,渲染引擎(浏览器内核)首先会对HTML文档进行解析,构建DOM树。解析HTML文档的同时会解析CSS样式(内部style标签下的CSS样式或外部的CSS文件),构建CSSOM树。HTML解析构建和CSS的解析是相互独立的并不会造成冲突,因此我们通常将css样式文件放在头文件head中,让浏览器尽早解析css。
由于js可能会改变html现有结构,因此当HTML解析过程遇到script标签时,渲染引擎会先暂停对DOM树的解析,单开一个线程调用JS引擎来加载js资源。等 JavaScript 引擎运行完毕,浏览器会从中断的地方继续构建DOM树。
默认情况下javascript是同步加载的,后面的元素要等待javascript加载完毕后才能进行加载(在计算机术语中,同步加载指单独一条线程加载,而异步加载指多条线程同时加载)。对于一些意义不是很大的javascript,如果放在页面头部加载会延迟页面首绘时间,严重影响用户体验。 因此可以在首绘不需要用到js的情况下将js的加载放在HTML文档底部,或使用async、异步API或promise对象等方式实现js的异步加载,防止js阻塞HTML文档的解析。
(JS本身是单线程的,在处理一个任务的时候无法同时去处理别的任务,单凭JS是无法实现异步编程的,必须借助一些别的机制。关于浏览器异步加载机制和异步操作事件,如DOM事件、ajax请求事件等,以后会用一个篇幅单独进行说明。)
当HTML解析完成后,浏览器会将文档标注为交互状态,并开始解析那些处于“deferred”模式的脚本,即那些应在文档解析完成后才执行的脚本。然后,文档状态将设置为“完成”,开始加载执行脚本文件。
当DOM树和CSSOM树构建完毕,就开始进行渲染树(Render Tree)的构建。渲染树是由DOM树和CSSOM树合并而成,它将网罗网页上所有可见的 DOM 内容,以及每个节点的所有 CSS 样式信息。
为构建渲染树,浏览器大体上完成了下列工作:
- 从dom树的根节点开始遍历每个可见节点
- 对于每个可见节点,为其找到适配的css规则并应用它们
- 发射可见节点,连同其内容和计算的样式
最终输出的渲染同时包含了屏幕上的所有可见内容及其样式信息。
(某些节点通过 CSS 移除,因此在渲染树中也会被忽略。注意 visibility: hidden 与 display: none 是不一样的。前者隐藏元素,但元素仍占据着布局空间,即将其渲染成一个空框,而后者 (display: none) 将元素从渲染树中完全移除,元素既不可见,也不是布局的组成部分。)
渲染树构建完毕之后,将会对元素进行放置。到目前为止,我们计算了哪些节点应该是可见的以及它们的计算样式,但我们尚未计算它们在设备视口内的确切位置和大小。对于元素的放置,Webkit内核 使用的术语是“布局Layout”,而 Gecko 内核称之为“重排/回流Reflow”。它是一个递归的过程,每个节点都负责自己及其子节点的布局,布局的结果是得到元素相对父节点的坐标和尺寸。渲染引擎精确地捕获每个元素在视口内的确切位置和尺寸,为每个节点提供它应该出现在屏幕上的确切坐标。
知道了哪些节点可见、它们的计算样式以及几何信息,我们可以最终将这些信息传递给最后一个阶段:将渲染树中的每个节点转换成屏幕上的实际像素,并将各个节点绘制到屏幕上,这一步通称为“绘制Painting”。
于是乎,一个网页最后就这么呈现在我们眼前了。
虽然百度的首页看起来很简单,但它的渲染却需要完成相当多的工作。如果 DOM 或 CSSOM 被修改,渲染引擎只能再执行一遍上述步骤,以确定哪些像素需要在屏幕上重新渲染。
(关于重绘Repaint和回流Reflow :当屏幕的一部分要重新粉饰,比如某个元素的背景颜色发生改变,但是元素的几何尺寸没有改变,这个时候就会进行重绘;而当元素的几何尺寸变了,我们需要重新验证并计算Render Tree,是Render Tree的一部分还是全部都发生了变化,这就是回流Reflow。Reflow 会从这个元素开始递归往下,依次重新计算所有结点的几何尺寸和位置。显然,回流Reflow要比重绘Repaint消耗的资源更多。)
渲染树构建、布局以及绘制所需要的加载时间取决于文档大小(如HTML文档)、应用的样式(CSS)和运行文档的设备:文档越大,浏览器需要完成的工作就越多;样式越复杂,绘制所需要的时间就越长。要理解这是一个渐进的过程,现代浏览器不会等到CSSOM树和DOM树全部构建完成才开始进行绘制,为了获得更好的用户体验,渲染引擎将尝试尽快在屏幕上显示内容。在开始构建和布局渲染树之前,它会先解析和显示部分HTML内容,同时继续处理来自网络的其余内容(如加载图片资源等)。所以在低网速的环境中,我们经常观察到页面至上而下缓慢的加载出来,或者是先显示文本内容后再绘制成带有样式的页面内容。
总的来说,在我们用浏览器浏览网站页面时,差不多进行了这样一个流程:
域名解析(DNS解析) ——> 发起TCP的3次握手 ——> 建立TCP连接后发起HTTP请求 ——>服务器响应HTTP请求,向浏览器返回HTML代码 ——>浏览器解析HTML代码,并加载HTML代码中的资源(如js、css、图片等)——> 浏览器对页面进行渲染呈现给用户
以上便是浏览器的渲染原理,当我们了解了浏览器底层是如何进行解析和渲染的,我们就可以开始思考如何优化我们的代码,使浏览器对页面的解析更高效。 我们在编写HTML文档和CSS样式时,应该尽可能的采取最优化的写法,以减少Repaint、Reflow对资源的消耗,提高浏览器的解析速率。至于网页浏览的具体优化方案,本文便不再阐述,以后会进一步为大家详细讲解。此外,此号还会陆续更新一些关于Web及算法方面的知识,欢迎各位志同道合的朋友一起留言探讨。