前言
在前端面试中,我们有时候会被问到一个经典的问题:当你在浏览器输入url的之后会发生什么?很多人可能不知道,从你输入网址到获取数据到渲染数据到页面,其实浏览器在背后默默地做了很多事情,这里就带大家了解一下浏览器的工作原理,看看浏览器是如何将数据渲染到页面上的吧。
一、浏览器的发展历程
浏览器的发展历程可以追溯到20世纪90年代初,当时的互联网还处于初期阶段,主要是以文字为主,没有融入多媒体元素。随着互联网的迅速发展,人们对浏览网页的需求也越来越迫切,于是各种浏览器开始出现。
1990年:蒂姆·伯纳斯-李发明了世界上第一个网页浏览器,最初命名为WorldWideWeb,后改名为Nexus,这个浏览器同时也是世界上第一款网页编辑器。
1993年:NCSA组织推出了能够显示图片的网页浏览器NCSA Mosaic,这是第一个普遍接受的具有图形界面的浏览器。
1994年:网景的浏览器Netscape Navigator发布,成为当时最受欢迎的浏览器,尤其在那个没有竞争对手的时代,Netscape Navigator几乎垄断了整个市场。
1995年:微软推出了Internet Explorer(IE)浏览器,由于其与Windows操作系统的捆绑销售,迅速占据了市场。
2003年:苹果发布Safari浏览器,后来开源了Safari浏览器的内核WebKit。
2004年:Mozilla推出了Firefox浏览器,成为Internet Explorer 的有力竞争者,受到广泛欢迎。
2008年:Google发布了Chrome浏览器,凭借其快速的性能和简洁的界面,迅速赢得了用户的青睐,一直到现在都在用户中占据大部分市场。
二、浏览器的基本结构和工作原理
-
浏览器的基本结构
用户界面:展示除标签页外的其他界面内容。
浏览器引擎:在用户界面和渲染引擎之间传递数据。
渲染引擎:负责渲染页面内容,包含多个模块内容,例如负责网络请求的网络模块、用于解析和执行JavaScript的JS解释器,以及数据存储持久层,用于帮助浏览器存储诸如Cookie等各类数据。
渲染引擎是浏览器的核心与灵魂,我们通常称之为内核,我们通常将渲染引擎称为浏览器的内核,不同浏览器使用的内核也有所不同。
不同浏览器的渲染引擎
IE:Trident。
Firefox:Gecko。
Safari:WebKit。
Chrome:Blink。
Opera和Edge:Blink。
浏览器的进程和线程
当我们启动某个程序时,操作系统会创建一个进程来执行任务代码。同时会为该进程分配内存空间,应用程序的状态均保存在该内存空间中。当应用关闭时,其占用的内存空间会被系统回收。进程可以启动更多的子进程来执行任务。由于每个进程分配的内存空间是独立的,如果两个进程间需要传递数据,则必须通过进程间通信管道(IPC)来实现。
进程:操作系统资源分配和调度的基本单元。
线程:操作系统进行运算调度的最小单位。
在现在的多进程浏览器出现之前,浏览器试是单进程的,所以会出现许多问题,诸如:一个进程卡死,整个页面卡死;或者执行某个页面时间耗费时间比较久,导致其他的操作被延迟;因为所有的任务都是按照顺序执行,并未实现真正意义上的并发,一个任务的卡死,就导致整个浏览器崩溃。现在的浏览器采用多进程结构,优化之前单进程浏览器出现的问题,同时也提高了浏览器的安全性,稳定性以及性能。
多进程结构的优势
稳定性:避免单个进程卡死影响整个浏览器。
安全性:进程间数据隔离。
流畅性:提高运行效率。
浏览器的多进程结构
多进程浏览器大致被分为五个部分,由五个不同的进程组成:
1. 浏览器进程:也被称为主进程,负责管理用户界面的内容,包括浏览器的前进、后退、地址栏、 书签栏。同时也控制浏览器的其他进程。
2. 渲染进程:负责执行js,解析html,css,绘制用户界面。每个标签页都有自己的渲染进程,渲染 进程之间相互独立,避免一个页面崩掉,影响到其他的标签页。
3. gpu进程:主要负责处理图形例如视频解码,3d渲染。
4. 网络进程:负责发起网络请求和响应。
5. 插件进程: 控制网站使用的插件,拓展文件。
三、浏览器渲染流程
网络线程获取数据
当你在地址栏输入网址时,浏览器进程的UI线程会捕捉你的输入内容。如果访问的是网址,UI线程会启动网络线程,请求DNS进行域名解析,随后连接服务器以获取数据。如果输入的是关键词而非网址,浏览器会识别为搜索请求,并使用默认配置的搜索引擎进行查询。当网络线程获取数据后,会通过Safe Browsing检查站点是否为恶意站点。若检测到安全问题,浏览器将展示警告页面并阻止访问,但您仍可选择强行继续访问。当返回数据准备完毕且安全校验通过时,网络线程会通知UI线程,表示数据已准备好。
渲染器进程的工作流
UI线程会创建一个渲染器进程来渲染页面,浏览器进程通过IPC管道将数据传递给渲染器进程,然后正式进入渲染流程。
-
解析HTML构建构建DOM树:浏HTML首先经过Tokenizer标记化,通过词法分析将输入的HTML内容解析成多个标记,并根据这些标记构建DOM树,每当解析到一个新的HTML标签时,相应的DOM节点就会被创建并添加到DOM树中,解析过程中,如果遇到外部资源(如图片、样式表或脚本),浏览器会同时请求这些资源。
-
加载与解析CSS构建CSSOM树:当浏览器遇到
<link>
或<style>
标签引用的CSS文件或内联样式时,它会下载并解析这些样式规则并生成CSSDOM树。这一步骤可能与HTML解析并行进行,取决于资源加载的方式和浏览器的策略。 -
生成渲染树:接下来,浏览器结合DOM树和CSSOM树来创建渲染树。需要注意的是,DOM树和渲染树并非一一对应。设置了`display: none`的节点不会出现在布局树上,而在`::before`伪类中添加了`content`值的元素,其内容会出现在布局树上,但不会出现在DOM树中。这是因为DOM是通过HTML解析获得的,并不涉及样式。最终由渲染树生成再屏幕上展示的节点。
-
布局(重排):一旦渲染树构建完成,浏览器便开始布局过程,即确定页面上每个元素的确切尺寸和位置。这一过程有时被称为“回流”,一旦涉及到对元素位置和大小的变更,就需要重新计算布局。
-
绘制:主线程遍历Layout Tree,根据渲染树中的信息,创建一个绘制记录表,该表记录了绘制的顺序,这个阶段被称为绘制。
-
合成:绘制表创建完成后,会将信息转化为像素点并显示在屏幕上,这一过程被称为栅格化,早起的谷歌浏览器使用了一种简单的方式,即栅格化用户可视区域的内容,当用户滚动页面时,浏览器会不断栅格化更多内容以填充缺失部分,这种方式带来的问题显而易见,会导致展示延迟。现在的Chrome采用了一种更为复杂的栅格化流程,称为合成。合成就是将页面的各个部分划分为多个图层,分别进行栅格化,最终将可视区域的内容组合成一帧展示给用户。
总结:浏览器进程中的网络线程请求获取到HTML数据后,通过IPC将数据传递给渲染器进程的主线程。主线程解析HTML并构建DOM树,随后进行样式计算。根据DOM树和生成的样式,主线程创建布局树(Layout Tree),并生成绘制顺序表。
接着,主线程将布局树和绘制顺序信息传递给合成器线程。合成器线程按照规则将图层分割,并将这些图层划分为更小的图块,传递给栅格线程进行栅格化处理。
栅格化完成后,合成器线程会接收栅格线程传递的图块信息。基于这些信息,合成器线程生成一个合成帧,并通过IPC将其传回浏览器进程。浏览器进程随后将帧传递给GPU进行渲染,最终显示在屏幕上。
四、回流和重绘
我们修改元素的尺寸或位置属性时,会触发样式计算、布局、绘制以及后续流程,这一过程称为重排。若仅改变元素的颜色属性,则不会触发布局,但仍会进行样式计算和绘制,这称为重绘。重排和重绘都会占用主线程。此外,GS(Graphics Layer)也运行在主线程上。由于它们共享主线程,可能会出现抢占执行时间的情况。
如果在运行动画时,还有大量的JavaScript任务需要执行,由于布局、绘制和JavaScript的执行都在主线程上进行,当一帧时间内布局和绘制完成后仍有剩余时间,JavaScript便会获得主线程的使用权。如果JavaScript执行时间过长,可能导致在下一帧开始时,JavaScript未能及时释放主线程,导致下一帧动画无法按时渲染,从而出现页面动画卡顿的现象。
优化手段
1、可以通过`requestAnimationFrame`这个API来解决这个问题。`requestAnimationFrame`会在每一帧被调用,我们可以将JavaScript任务拆分为更小的任务块,在每一帧时间用完前暂停JavaScript的执行,释放主线程,这样在下一帧开始时,主线程便可以按时执行布局和绘制。
2、通过之前的流程图可知,合成器的整个流程不占用主线程,仅在合成器线程和栅格线程中运行,这意味着它无需与JavaScript争夺主线程资源。CSS中有个动画属性叫`transform`,通过该属性实现的动画不会经过布局和绘制,而是直接运行在合成器线程和栅格化线程中,因此不会受到主线程中JS执行的影响,通过`transform`实现的动画,不需要经过布局、绘制、样式计算等操作,可以节省了大量运算时间。