作为前端开发,浏览器是我们代码运行的主要环境,因此了解浏览器的原理是非常必要的。
本文从以下几个方面介绍浏览器的原理:
- 浏览器的组成
- 浏览器的网络
- 浏览器的渲染过程
我们从一道经典的面试题开始:浏览器中输入一段信息,点回车后,发生了什么?。在这个过程中浏览器做了如下几个方面的事。
1.响应用户输入,浏览器界面状态改变
2.浏览器网络进程处理请求
3.响应请求接收返回信息
4.根据返回信息重新渲染界面
5.浏览器状态更新
其中第1、5阶段主要为输入数据的解析,以及浏览器导航栏,url输入栏等的改变等。本文重点关注2, 3, 4 三个阶段。
1、浏览器的组成部分
当前主流浏览器的主要由:浏览器进程、渲染进程、网络进程、GPU进程、插件进程组成,其中插件进程主要处理插件相关内容,本文不做介绍;GPU进程主要用来进行3D渲染,以及参与渲染过程中的光栅化等;网络进程主要用来发送接受请求等;浏览器进程负责子进程管理、响应用户输入、提供存储等功能。渲染进程核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎Blink和JavaScript引擎V8都是运行在该进程中,默认情况下,Chrome会为每个Tab标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。
对应上文的面试题,过程如下图,摘自(参考文献3)
首先介绍网络过程
2、浏览器的网络
网络进程在发起请求之前,会检查本地是否有缓存的资源,并根据缓存情况来决定是否需要发送请求。缓存可能来自以下几个位置:
Service Worker
Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用 Service Worker的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全。Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。
Service Worker 实现缓存功能一般分为三个步骤:首先需要先注册 Service Worker,然后监听到 install 事件以后就可以缓存需要的文件,那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。
当 Service Worker 没有命中缓存的时候,我们需要去调用 fetch 函数获取数据。也就是说,如果我们没有在 Service Worker 命中缓存的话,会根据缓存查找优先级去查找数据。但是不管我们是从 Memory Cache 中还是从网络请求中获取的数据,浏览器都会显示我们是从 Service Worker 中获取的内容。
MemoryCache/DiskCache
资源类型可以大致分为主资源:如html文件,下载链接等;派生资源:html中嵌入的js, css,图片等。通常缓存来自MemoryCache、DiskCache二者都是只能缓存派生资源,并且和浏览器的实现有关,通常webkit内核的浏览器中通过地址栏回车或刷新按钮浏大概率会走MemoryCache或者DiskCache,并且内存缓存在缓存资源时并不关心返回资源的HTTP缓存头Cache-Control是什么值,同时资源的匹配也并非仅仅是对URL做匹配,还可能会对Content-Type,CORS等其他特征做校验。通常关闭tab将会清空MemoryCache的内容(具体根据什么判断逻辑走哪一个,暂时还没查到权威资料),在浏览器控制面板会看到from memory cache或者from disk cache。这些缓存可能和浏览器的实现有关如Chrome和firefox是不同的。memory cache是浏览器为了优化性能做的设计,但是内存有限,浏览器只能将部分资源放在内存中,比较大的资源会被放在disk cache中。而且内存中的资源是会随着内存使用情况进行清除的。一般资源如果没有设置强制验证或者不缓存,并且内存中有相应的资源,就会走内存,否则会根据response中的缓存策略。默认情况下,如果没有配置缓存策略,浏览器也会缓存一段时间,具体多长时间,则会根据response中返回的,last-modified, date等来确定,一般为时间差的0.1倍。其他缓存策略参考:mdn
如果没有缓存,则会发起请求,首先解析DNS,获得IP地址,根据IP地址来和服务端进行握手,建立tcp连接,如果是https请求,还需要建立tls连接等,总之如果CDN等代理没有缓存信息则会请求到源站,经过服务端处理,返回相应浏览器网络进程根据返回的信息进行不同的处理。
如果服务端返回状态码301等,会同时返回Location则会发起重定向,重新请求location对应的url。如果是正常的返回,接下来浏览器根据返回的content-type的类型来决定是渲染页面,还是下载资源等。如果是text/html则会渲染一个页面。接下来浏览器会为页面创建一个渲染进程(也有可能由于当前域名已经有网页打开而复用已打开页面对应的渲染进程),并且开始去修改浏览器的加载状态,安全状态,并开始用新的内容覆盖原浏览器页面中的内容。
3、浏览器的渲染过程
渲染引擎的主要流程大致是获取并解析HTML文档构建DOM树,之后创建render树,render树包含有视觉属性(如颜色和尺寸),最后进入布局和绘制阶段。需要指出的是,这是个复杂和渐进的过程,为了更好地用户体验,渲染引擎通常会先将解析完成的部分HTML显示出来。浏览器的渲染过程大体分为如下几个步骤。
html解析 ---> 样式计算 ---> 布局 ---> 分层 ---> 绘制 ---> 光栅化 --->合成
html解析
从服务端请求回来的html文件只是一堆字符,浏览器要做的就是将这些字符解析为有意义的语法树来供下一阶段使用。这其中经过了词法分析(解析出不同的token等),语法分析(将一系列的token解析为有意义的语法树,使其具有语义)。解析器遇到script标记时会立即解析并执行脚本。文档的解析将停止,直到脚本执行完毕。对于WebKit而言,仅当脚本尝试访问的样式属性可能受尚未加载的样式表影响时,它才会禁止该脚本,其他情况下样式的加载不会影响html的解析。
html解析的输出是dom树,dom树是保留在内存中,可以通过脚本访问dom树,并对其进行操作,与此同时会构建render树,dom树和dom节点是一一对应的而render树却不是,render树中只会保留可见的dom元素,并且是按照元素实际在浏览器窗口中的展示顺序排列。head元素 display为none的元素都不会出现在render树中,render树是用来进行渲染的。在构建render树的时候需要进行css样式计算。
样式计算
样式主要来源于html中link的外部文件,style元素中的样式规则,以及元素的style元素中包含的样式。对于这些样式,浏览器首先会对齐样式进行标准化,比如尺寸单位,rem,百分比等会被标准化为px,red会被标准化为rgb(255,0,0)。另外,对于没有被样式文件,样式属性显式设置的元素,如果该属性是默认被继承的,则会使用继承自父亲或祖先的属性,如果不是默认被继承的则使用默认属性(initial value)来作为其样式。inherit 关键字允许显式的声明继承性,它对继承和非继承属性都生效,也就是说所有的属性都是可以被继承的,只是有些是默认继承父亲,有些默认不继承,使用initial value。这个过程中浏览器要做的就是将这些规则匹配到对应的元素中,并根据规则的优先级,style ID class tag 等最终确定应用哪个规则。这个过程是十分复杂的,此处我们不再做深入探讨,有感性认识即可。
布局
布局阶段就是根据样式中的位置,尺寸信息,设定元素大小和位置。布局分为全局和局部两种,很好理解,比如修改了根节点的font-size,如果子节点继承了根节点的font-size则会进行整体布局,如果只是某个绝对定位的元素的宽高改变,则不会影响其他布局流中的元素的。
分层
通常render树中的元素会有层叠关系,具有层叠上下文的元素会分层渲染,除此之外,被裁剪的元素也会分层渲染。
绘制
绘制的过程是把图层的绘制分解为很多的小的指令,如,绘制背景,绘制边框,绘制内容,绘制阴影等等,注意这个过程只是绘制指令的编排行成绘制列表,并不是真正在屏幕上展示。
光栅化
当图层的绘制列表准备好之后,主线程会把该绘制列表提交(commit)给合成线程,通常由于浏览器窗口比较大,如果全部绘制会影响性能,并且一些不可见区域的绘制也是没有必要的。因此需要进行分块。光栅化(Rasterize)就是将一些矢量形状转换为位图(Raster Image)形式。经过这样的变换后,这些形状才可以在屏幕上进行显示,也可以被打印机打印出来。将在布局阶段计算的每个框转换为屏幕上的实际像素。此过程有GPU进程参与。光栅化
合成
一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。
浏览器进程里面有一个叫viz的组件,用来接收合成线程发过来的DrawQuad命令,然后根据DrawQuad命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。
总结渲染过程如下图:
提升网页性能的好习惯
影响网页性能的常见操作:
1.重绘:改变颜色,透明度,outline(注意区分border)
2.重排:不同行为的性能损耗有差异。不同浏览器对不同行为所造成的性能损耗也不同。
- size, 位置
- 重建或者删除元素
- 直接修改style或者class可能导致。
- css动画
- 用户行为:hover, 文本框输入,页面缩放,页面滚动。
好的习惯:
-
尽量不使用行内style,会导致额外的重排
-
尽量不使用table
-
在页面的主要元素处,尽量不是用flex布局,因为弹性布局的元素在页面下载下来后,位置和大小可能会发生改变。
-
不要一条条改变样式,使用class来批量修改
-
使用更少的css规则
-
降低DOM深度
-
如果需要修改dom class,尽量修改层级低的元素。
-
将动画从文档流中移走,使用绝对定位,其重拍的消耗会比较小
-
给display:none的元素设置属性后再 使其可见。
-
需要修改多个元素属性时候,批量修改。
-
尽量使改动后影响到的元素数量少。
-
每次移动元素的长度可以适当长一些,对效果影响小,但是对性能提升大。
参考文献:
2.zhuanlan.zhihu.com/p/80551769
3.blog.poetries.top/browser-wor…
4.shenlvmeng.github.io/blog/2017/0…