身为前端,打交道最多的就是浏览器和node了,也是咱们必须熟悉的。接下来咱们讲一下浏览器工做原理和工做过程。从url到页面的过程,......,咱们直接来到收到服务器返回内容部分开始。css
先上不少人都见过的一幅图: html
还有一幅图: 前端
浏览器主要组成部分:node
body
元素的width变化会影响其后代元素的宽度。所以,布局过程是常常发生的。词法分析器将输入内容分解成一个个有效标记,解析器负责根据语言的语法规则分析文档的结构来构建解析树。词法分析器知道如何将无关的字符(空格、换行符等)分离出来,因此咱们平时写一些空格也不会影响大局。git
在语法分析的过程当中,解析器会向词法分析器请求一个标记(就是前面分解出来的标记),并尝试将其与某条语法规则(好比标签要闭合、正确嵌套)进行匹配。若是发现了匹配规则,解析器会将一个对应于该标记的节点添加到解析树中,而后继续请求下一个标记。github
若是没有规则能够匹配,解析器就会将标记存储到内部,并继续请求标记,直至找到可与全部内部存储的标记匹配的规则(如div多层嵌套的状况,这样子能找到div闭合部分)。若是找不到任何匹配规则,解析器就会引起一个异常。这意味着文档无效,包含语法错误。算法
解析器类型有两种:json
编译:将源代码编译成机器代码,源代码先走完解析的过程造成成解析树,解析树被翻译成机器代码文档,完成编译的过程后端
特殊的是,刚好html不能用上面两种解析方法。有一种能够定义 HTML 的正规格式:DTD,但它不是与上下文无关的语法,html明显是和上下文关系紧密的。咱们知道 HTML 是有点“随意”的,对于不闭合的或者不正确嵌套标签有可能不报错,而且尝试解释成正确的样子,具备必定的容错机性,所以能够达到简化网络开发的效果。另外一方面,这使得它很难编写正式的语法。归纳地说,HTML 没法很容易地经过常规解析器解析(由于它的语法不是与上下文无关的语法),因此采用了 DTD 格式。api
解析器解析html文档的解析树是由 DOM 元素和属性节点构成的树结构。它是 HTML 文档的对象表示,同时也是外部内容(例如 JavaScript)与 HTML 元素之间的api,其根节点是document。上面已经说到,不能使用常规的解析技术解释html,浏览器就建立了自定义的解析器来解析 。对于HTML/SVG/XHTML这三种文档,Webkit有三个C++的类对应这三种文档,并产生一个DOM Tree。解释html成dom的过程,由两个阶段组成:标记化和树构建。
对于一段html:
<html>
<body>
hi
</body>
</html>
复制代码
该算法使用状态机来表示。每个状态接收来自输入信息流的一个或多个字符,并根据这些字符更新下一个状态。当前的标记化状态和树结构状态会影响进入下一状态的决定。
初始状态是数据状态。遇到字符 < 时,状态更改成“标记打开状态”。接收一个字母会建立“起始标记”,状态更改成“标记名称状态”。这个状态会一直保持到接收 > 字符,接收到将会进入“标记打开状态”。在此期间接收的每一个字符都会附加到新的标记名称上。
好比咱们先写html标签,先遇到<,进入“标记打开状态”,遇到html四个字母进入“标记名称状态”,接着接收到了>字符,会发送当前的标记,状态改回“数据状态”
<body>
标记也会进行一样的处理。如今 html 和 body 标记均已发出,并且目前是“数据状态”。接收到 hi中的 h 字符时,将建立并发送字符标记,直到接收 </body>
中的 <。咱们将为hi的每一个字符都发送一个字符标记。
回到“标记打开状态”。接收下一个输入字符 / 时,会建立闭合标签token,并改成“标记名称状态”。咱们会再次保持这个状态,直到接收 >。而后将发送新的标记,并回到“数据状态”。最后,</html>
输入也会进行一样的处理。
在建立解析器的同时也会建立 document 对象。在树构建阶段,以 Document 为根节点的 DOM 树也会不断进行修改,向其中添加各类元素。标记生成器发送的每一个节点都会由树构建器进行处理。
树构建阶段的输入是一个来自标记化阶段的标记序列。第一个模式是“initial mode”。接收 HTML 标记后转为“before html”模式,并在这个模式下从新处理此标记。这样会建立一个 HTMLHtmlElement 元素,并将其附加到 Document 根对象上。
状态改成“before head”。此时咱们接收“body”标记。因为容错性,就算咱们的没head标签,系统也会隐式建立一个 HTMLHeadElement,并将其添加到树中。
进入了“in head”模式,而后转入“after head”模式。系统对 body 标记进行从新处理,建立并插入 HTMLBodyElement,同时模式转变为“in body”。
接收由“hi”字符串生成的一系列字符标记。接收第一个字符时会建立并插入文本节点,而其余字符也将附加到该节点。固然还有其余节点,好比属性节点、换行节点。咱们实际场景还有外部资源以及其余各类各样的复杂标签嵌套和内容结构,不过原理都相似。对于中间这个过程,遇到外部资源如何处理,顺序是怎样的,后面再讲。
接收 body 结束标记会触发“after body”模式。如今咱们将接收 HTML 结束标记,而后进入“after after body”模式。接收到文件结束标记后,解析过程就此结束,dom树已经创建完毕(不是加载完毕,在DOMContentLoaded以前,document.readyState = ‘interactive ’)。
结束后,此时文档被标注为交互状态,浏览器开始解析那些script标签上带有“defer”脚本,也就是那些应在文档解析完成后才执行的脚本,文档状态将设置为“完成”,执行完毕触发DOMContentLoaded事件(当初始的 HTML 文档被彻底加载和解析完成以后,DOMContentLoaded 事件被触发,不会等待样式表、图像和iframe的完成加载)。
解析CSS会产生CSS规则树,前面已经说到,html不是与上下文无关的语法,而css和js是与上下文无关的语法,因此常规的解析方法均可以用。对于创建CSS 规则树,是须要比照着DOM树来的。CSS匹配DOM树主要是从右到左解析CSS选择器。解析CSS的顺序是浏览器的样式 -> 用户自定义的样式 -> 页面的link标签等引进来的样式 -> 写在style标签里面的内联样式
样式表不会更改 DOM 树,所以没有必要等待样式表并中止文档解析。而脚本在文档解析阶段会请求样式信息时尚未加载和解析样式,脚本就会得到错误的回复。Firefox 在样式表加载和解析的过程当中,会禁止全部脚本。而对于 WebKit 而言,仅当脚本尝试访问的样式属性可能受还没有加载的样式表影响时,它才会禁止该脚本。
网络整个解析的过程是同步的,会暂停 DOM 的解析。解析器遇到 script标记时当即解析并执行脚本。文档的解析将中止,直到脚本执行完毕。
若是脚本是外部的,那么解析过程会中止,直到从网络同步抓取资源完成后再继续。
目前浏览器的script标签是并行下载的,他们互相之间不会阻塞,可是会阻塞其余资源(图片)的下载
因此为了用户体验,后来有了async和defer,将脚本标记为异步,不会阻塞其余线程解析和执行。标注为“defer”的script不会中止文档解析,而是等到解析结束才执行;标注为“async”只能引用外部脚本,下载完立刻执行,并且不能保证加载顺序。
脚本的预解析:在执行脚本时,其余线程会解析文档的其他部分,找出并加载须要经过网络加载的其余资源。经过这种方式,资源能够在并行链接上加载,从而提升整体速度。请注意,预解析器不会修改 DOM 树,而是将这项工做交由主解析器处理;预解析器只会解析外部资源(例如外部脚本、样式表和图片)的引用。
脚本主要是经过DOM API和CSSOM API来操做DOM Tree和CSS Rule Tree.
另外,咱们又能够想到一个问题,为何jsonp能response一个类eval字符串就立刻执行呢?其实也是由于普通的script标签解析完成就立刻执行,咱们在服务器那边大概是这样子返回: res.end('callback('+data+')')
整个过程,就是:动态建立script标签,src为服务器的一个get请求接口,遇到src固然立刻请求服务器,而后服务器返回处理data的callback函数这样子的代码。其实,咱们能够看做是前端发get请求,服务端响应文档是js文件,并且这个文件只有一行代码:callback(data)。固然你能够写不少代码,不过通常没见过有人这么干。
html、css、js解析完成后,浏览器引擎会经过DOM Tree 和 CSS Rule Tree 来构造 Rendering Tree(渲染树)。
有一些 DOM 元素对应多个可视化对象。它们每每是具备复杂结构的元素,没法用单一的矩形来描述。如“select”元素有 3 个呈现器:一个用于显示区域,一个用于下拉列表框,还有一个用于按钮。若是因为宽度不够,文本没法在一行中显示而分为多行,那么新的行也会做为新的呈现器而添加。
inline 元素只能包含 block 元素或 inline 元素中的一种。若是出现了混合内容,则应建立匿名的 block 呈现器,以包裹 inline 元素。因此咱们平时的inline-block能够设置宽高。
有一些呈现对象对应于 DOM 节点,但在树中所在的位置与 DOM 节点不一样。脱离文档流的浮动定位和绝对定位的元素就是这样,被放置在树中的其余地方,并映射到真正的frame,而放在原位的是占位frame。
构建渲染树以前,须要计算每个呈现对象的可视化属性。这是经过计算每一个元素的样式属性来完成的。
Firefox:CSS 解析生成 CSS Rule Tree,经过比对DOM生成Style Context Tree,而后Firefox经过把Style Context Tree和其Render Tree(Frame Tree)关联上完成样式计算
Webkit:把Style对象直接存在了相应的DOM结点上了
样式被js改变过的话,会从新计算样式(Recalculate Style)。Recalculate被触发的时,处理脚本给元素设置的样式。Recalculate Style会计算Render树(渲染树),而后从根节点开始进行页面渲染,将CSS附加到DOM上的过程。因此任何企图改变元素样式的操做都会触发Recalculate,在JavaScript执行完成后才触发的,下面将会讲到的layout也是。
Firefox:系统会针对 DOM 更新注册展现层,做为侦听器。展现层将框架建立工做委托FrameConstructor,由该构造器解析样式并建立frame。
WebKit:解析样式和建立呈现器的过程称为“附加”。每一个 DOM 节点都有一个“attach”方法。附加是同步进行的,将节点插入 DOM 树须要调用新的节点“attach”方法。
处理 html 和 body 标记就会构建渲染树根节点。这个根节点呈现对象对应于 CSS 规范中所说的容器 block,这是最上层的 block,包含了其余全部 block。它的尺寸就是视口,即浏览器窗口显示区域的尺寸。Firefox 称之为 ViewPortFrame,而 WebKit 称之为 RenderView。这就是文档所指向的呈现对象。渲染树的其他部分以 DOM 树节点插入的形式来构建。
呈现器在建立完成并添加到渲染树时,并不包含位置和大小信息。**计算这些值的过程**称为布局(layout)或重排(repaint)。这个得记住了,记准确了!为何呢?计算offsetWidth和offsetHeight的、js操做dom、改变style属性时候,都会引起重排!
前面经过样式计算肯定了每一个DOM元素的样式,这一步就是具体计算每一个DOM元素最终在屏幕上显示的大小和位置。Web页面中元素的布局是相对的,所以一个元素的布局发生变化,会联动地引起其余元素的布局发生变化。好比,元素的width变化会影响其后代元素的宽度。所以,layout过程是常常发生的。
HTML 是流式布局,这意味着大多数状况下只要一次遍历就能计算出几何信息。处于流中靠后位置元素一般不会影响靠前位置元素的几何特征,所以布局能够按从左至右、从上至下的顺序遍历文档。坐标系是相对于根节点而创建的,使用的是上坐标和左坐标。根呈现器的位置左边是 0,0,其尺寸为视口。layout过程计算一个元素绝对的位置和尺寸。Layout计算的是布局位置信息。任何有可能改变元素位置或大小的样式都会触发这个Layout事件。
layout是一个递归的过程。它从根呈现器(对应于 HTML 文档的 元素)开始,而后递归遍历部分或全部的框架层次结构,为每个须要计算的呈现器计算几何信息。全部的呈现器都有一个“layout”或者“reflow”方法,每个呈现器都会调用其须要进行布局的子代的 layout 方法。任何有可能改变元素位置或大小的样式都会触发这个Layout事件。
因为元素相覆盖,相互影响,稍有不慎的操做就有可能致使一次自上而下的布局计算。因此咱们在进行元素操做的时候要一再当心尽可能避免修改这些从新布局的属性。当你修改了元素的样式(好比width、height或者position等)也就是修改了layout,那么浏览器会检查哪些元素须要从新布局,而后对页面激发一个reflow过程完成从新布局。被reflow的元素,接下来也会激发绘制过程也就是重绘(repaint),最后激发渲染层合并过程,生成最后的画面。因为元素相覆盖,相互影响,稍有不慎的操做就有可能致使一次自上而下的布局计算。因此咱们在进行元素操做的时候要一再当心尽可能避免修改这些从新布局的属性。
若是呈现器在布局过程当中须要换行,会当即中止布局,并告知其父代须要换行。父代会建立额外的呈现器,并对其调用布局。
为避免对全部细小更改都进行总体布局,浏览器采用了一种“dirty 位”系统。若是某个呈现器发生了更改,或者将自身及其子代标注为“dirty”,则须要进行布局。相似于脏检测。
有“dirty”和“children are dirty”两种标记方法。“children are dirty”表示尽管呈现器自身没有变化,但它至少有一个子代须要布局。dirty就是本身都变化了。
当呈现器为 dirty 时,会异步触发增量布局。例如,当来自网络的额外内容添加到 DOM 树以后,新的呈现器附加到了呈现树中。
增量布局是异步执行的。Firefox 将增量布局的“reflow 命令”加入队列,而调度程序会触发这些命令的批量执行。WebKit 也有用于执行增量布局的计时器:对呈现树进行遍历,并对 dirty 呈现器进行布局。 请求样式信息(例如“offsetHeight”)的脚本可同步触发增量布局。 全局布局每每是同步触发的。 有时,当初始布局完成以后,若是一些属性(如滚动位置)发生变化,布局就会做为回调而触发。
若是布局是由“大小调整”或呈现器的位置(而非大小)改变而触发的,那么能够从缓存中获取呈现器的大小,而无需从新计算。 在某些状况下,只有一个子树进行了修改,所以无需从根节点开始布局。这适用于在本地进行更改而不影响周围元素的状况,例如在文本字段中插入文本(不然每次键盘输入都将触发从根节点开始的布局)。
由于这个优化方案,因此你每改一次样式,它就不会reflow或repaint一次。可是有些状况,若是咱们的程序须要某些特殊的值,那么浏览器须要返回最新的值,而会有一些样式的改变,从而形成频繁的reflow/repaint。好比获取下面这些值,浏览器会立刻进行reflow:
offsetTop, offsetLeft, offsetWidth, offsetHeight scrollTop/Left/Width/Height clientTop/Left/Width/Height getComputedStyle(), currentStyle
你们滚瓜烂熟的老话,再啰嗦一遍:尽可能减小重绘重排。具体:
重排(也叫回流)会计算页面布局(Layout)。某个节点Reflow时会从新计算节点的尺寸和位置,并且还有可能触其后代节点reflow。重排后,浏览器会从新绘制受影响的部分到屏幕,该过程称为重绘。另外,DOM变化不必定都会影响几何属性,好比改变一个元素的背景色不影响宽高,这种状况下只会发生重绘,代价较小。
当DOM的变化影响了元素的几何属性(宽或高),浏览器须要从新计算元素的几何属性,因为流式布局其余元素的几何属性和位置也受到影响。浏览器会使渲染树中受到影响的部分失效,并从新构造渲染树。 reflow 会从根节点开始递归往下,依次计算全部的结点几何尺寸和位置,在reflow过程当中,可能会增长一些frame,如文本字符串。DOM 树里的每一个结点都会有reflow方法,一个结点的reflow颇有可能致使子结点,甚至父点以及同级结点的reflow。
当渲染树的一部分(或所有)由于元素的尺寸、布局、隐藏等改变而须要从新构建。因此,每一个页面至少须要一次reflow,就是页面第一次加载的时候。
repaint(重绘)遍历全部节点,检测节点的可见性、颜色、轮廓等可见的样式属性,而后根据检测的结果更新页面的响应部分。当渲染树中的一些元素须要更新一些不会改变元素不局的属性,好比只是影响元素的外观、风格、而不会影响布局的那些属性,这时候就只发生重绘。固然,页面首次加载也是要重绘一次的。
光栅:光栅主要是针对图形的一个栅格化过程。现代浏览器中主要的绘制工做主要用光栅化软件来完成。因此元素重绘由这个元素和绘制层级的关系,来决定的是否会很大程度影响你的性能-,若是这个元素盖住的多层元素都被从新绘制,性能损耗固然大。
在绘制阶段,系统会遍历渲染树,并调用呈现器的“paint”方法,将呈现器的内容绘制成位图。绘制工做是使用用户界面基础组件完成的 你所看见的一切都会触发paint。包括拖动滚动条,鼠标选择中文字等这些彻底不改变样式,只改变显示结果的动做都会触发paint。paint的工做就是把文档中用户可见的那一部分展示给用户。paint是把layout和样式计算的结果直接在浏览器视窗上绘制出来,它并不实现具体的元素计算,只是layout后面的那一步。
绘制顺序:背景颜色->背景图片->边框->子代->轮廓
其实就是元素进入堆栈样式上下文的顺序。这些堆栈会从后往前绘制,所以这样的顺序会影响绘制。
再说回来,在样式发生变化时,浏览器会尽量作出最小的响应。所以,元素的颜色改变后,只会对该元素进行重绘。元素的位置改变后,只会对该元素及其子元素(可能还有同级元素)进行布局和重绘。添加 DOM 节点后,会对该节点进行布局和重绘。一些重大变化(例如增大“html”元素的字体)会致使缓存无效,使得整个渲染树都会进行从新布局和绘制。
概念不复杂,便是渲染层合并,咱们将渲染树绘制后,造成一个个图层,最后把它们组合起来显示到屏幕。渲染层合并。前面也说过,对于页面中DOM元素的绘制是在多个层上进行的。在每一个层上完成绘制过程以后,浏览器会将绘制的位图发送给GPU绘制到屏幕上,将全部层按照合理的顺序合并成一个图层,而后在屏幕上呈现。
对于有位置重叠的元素的页面,这个过程尤为重要,由于一量图层的合并顺序出错,将会致使元素显示异常。另外,这部分主要的是这涉及到咱们常说的GPU加速的问题。
说到性能优化,针对页面渲染过程的话,咱们但愿的是代价最小,避免多余的性能损失,少一点让浏览器作的步骤。好比咱们能够分析一下开头的那幅图:
明显,咱们改的越深,代价越大,因此咱们只改最后一个流程——合成的时候,性能是最好的。浏览器会为使用了transform或者animation的元素单首创建一个层。当有单独的层以后,此元素的Repaint操做将只须要更新本身,不用影响到别,局部更新。因此开启了硬件加速的动画会变得流畅不少。
由于每一个页面元素都有一个独立的渲染进程,包含了主线程和合成线程,主线程负责js的执行、CSS样式计算、计算Layout、将页面元素绘制成位图(Paint)、发送位图给合成线程。合成线程则主要负责将位图发送给GPU、计算页面的可见部分和即将可见部分(滚动)、通知GPU绘制位图到屏幕上。加上一个点,GPU对于动画图形的渲染处理比CPU要快,那么就能够达到加速的效果。
注意不能滥用GPU加速,必定要分析其实际性能表现。由于GPU加速建立渲染层是有代价的,每建立一个新的渲染层,就意味着新的内存分配和更复杂的层的管理。而且在移动端 GPU 和 CPU 的带宽有限制,建立的渲染层过多时,合成也会消耗跟多的时间,随之而来的就是耗电更多,内存占用更多。过多的渲染层来带的开销而对页面渲染性能产生的影响,甚至远远超过了它在性能改善上带来的好处。
这是补充前面的html解析为dom部分的内容。
明显,CSSOM树和DOM树是互不关联的两个过程。平时咱们把link标签放部头而script放body尾部,由于js阻塞阻塞DOM树的构建。可是js须要查询CSS信息,因此js还要等待CSSOM树构建完才能够执行。这就形成CSS阻塞了js,js阻塞了DOM树构建。因此咱们只要设置link的preload来预加载css文件,解决了js执行时CSSOM树还没构建好的阻塞问题。固然,script异步加载也是另外的方法。
总的来讲,参考一下不少人说过的规律: