从渲染原理谈前端性能优化

前言

合格的开发者知道怎么作,而优秀的开发者知道为何这么作。
这句话来自《web性能权威指南》,我一直很喜欢,而本文尝试从浏览器渲染原理探讨如何进行性能提高。
全文将从网络通讯以及页面渲染两个过程去探讨浏览器的行为及在此过程当中咱们能够针对那些点进行优化,有些的不足之处还请各位不吝雅正。css


1、关于浏览器渲染的容易误解点总结

关于浏览器渲染机制已是老生常谈,并且网上现有资料中有很是多的优秀资料对此进行阐述。遗憾的是网上的资料参差不齐,常常在不一样的文档中对同一件事的描述出现了极大的差别。怀着严谨求学的态度通过大量资料的查阅和请教,将会在后文总结出一个完整的流程。html

一、DOM树的构建是文档加载完成开始的?
DOM树的构建是从接受到文档开始的,先将字节转化为字符,而后字符转化为标记,接着标记构建dom树。
这个过程被分为标记化和树构建
而这是一个渐进的过程。为达到更好的用户体验,呈现引擎会力求尽快将内容显示在屏幕上。它没必要等到整个 HTML 文档解析完毕以后,就会开始构建呈现树和设置布局。在不断接收和处理来自网络的其他内容的同时,呈现引擎会将部份内容解析并显示出来。
参考文档:http://taligarsiel.com/Projec...前端

二、渲染树是在DOM树和CSS样式树构建完毕才开始构建的吗?
这三个过程在实际进行的时候又不是彻底独立,而是会有交叉。会形成一边加载,一边解析,一边渲染的工做现象。
参考文档:http://www.jianshu.com/p/2d52...linux

三、css的标签嵌套越多,越容易定位到元素
css的解析是自右至左逆向解析的,嵌套越多越增长浏览器的工做量,而不会越快。
由于若是正向解析,例如「div div p em」,咱们首先就要检查当前元素到 html 的整条路径,找到最上层的 div,再往下找,若是遇到不匹配就必须回到最上层那个 div,往下再去匹配选择器中的第一个 div,回溯若干次才能肯定匹配与否,效率很低。
逆向匹配则不一样,若是当前的 DOM 元素是 div,而不是 selector 最后的 em,那只要一步就能排除。只有在匹配时,才会不断向上找父节点进行验证。
打个好比 p span.showing
你认为从一个p元素下面找到全部的span元素并判断是否有class showing快,仍是找到全部的span元素判断是否有class showing而且包括一个p父元素快
参考文档:http://www.imooc.com/code/4570nginx


2、页面渲染的完整流程

当浏览器拿到HTTP报文时呈现引擎将开始解析 HTML 文档,并将各标记逐个转化成“内容树”上的 DOM 节点。同时也会解析外部 CSS 文件以及样式元素中的样式数据。HTML 中这些带有视觉指令的样式信息将用于建立另外一个树结构:呈现树。浏览器将根据呈现树进行布局绘制。web

  以上就是页面渲染的大体流程。那么浏览器从用户输入网址以后到底作了什么呢?如下将会进行一个完整的梳理。鉴于本文是前端向的因此梳理内容会有所偏重。而从输入到呈现能够分为两个部分:网络通讯页面渲染算法


咱们首先来看网络通讯部分:chrome

一、用户输入url并敲击回车。
二、进行DNS解析。
若是用户输入的是ip地址则直接进入第三条。但去记录毫无规律且冗长的ip地址显然不是易事,因此一般都是输入的域名,此时就会进行dns解析。所谓DNS(Domain Name System)指域名系统。因特网上做为域名和IP地址相互映射的一个分布式数据库,可以使用户更方便的访问互联网,而不用去记住可以被机器直接读取的IP数串。<font color='#39495c' size='3'>经过主机名,最终获得该主机名对应的IP地址的过程叫作域名解析(或主机名解析)。</font>这个过程以下所示:数据库

浏览器会首先搜索浏览器自身的DNS缓存(缓存时间比较短,大概只有2分钟左右,且只能容纳1000条缓存)。跨域

  • 若是浏览器自身缓存找不到则会查看系统的DNS缓存,若是找到且没有过时则中止搜索解析到此结束.
  • 而若是本机没有找到DNS缓存,则浏览器会发起一个DNS的系统调用,就会向本地配置的首选DNS服务器发起域名解析请求(经过的是UDP协议向DNS的53端口发起请求,这个请求是递归的请求,也就是运营商的DNS服务器必须得提供给咱们该域名的IP地址),运营商的DNS服务器首先查找自身的缓存,找到对应的条目,且没有过时,则解析成功。
  • 若是没有找到对应的条目,则有运营商的DNS代咱们的浏览器发起迭代DNS解析请求,它首先是会找根域的DNS的IP地址(这个DNS服务器都内置13台根域的DNS的IP地址),找打根域的DNS地址,就会向其发起请求(请问www.xxxx.com这个域名的IP地址是多少啊?)
  • 根域发现这是一个顶级域com域的一个域名,因而就告诉运营商的DNS我不知道这个域名的IP地址,可是我知道com域的IP地址,你去找它去,因而运营商的DNS就获得了com域的IP地址,又向com域的IP地址发起了请求(请问www.xxxx.com这个域名的IP地址是多少?),com域这台服务器告诉运营商的DNS我不知道www.xxxx.com这个域名的IP地址,可是我知道xxxx.com这个域的DNS地址,你去找它去,因而运营商的DNS又向linux178.com这个域名的DNS地址(这个通常就是由域名注册商提供的,像万网,新网等)发起请求(请问www.xxxx.com这个域名的IP地址是多少?),这个时候xxxx.com域的DNS服务器一查,诶,果然在我这里,因而就把找到的结果发送给运营商的DNS服务器,这个时候运营商的DNS服务器就拿到了www.xxxx.com这个域名对应的IP地址,并返回给Windows系统内核,内核又把结果返回给浏览器,终于浏览器拿到了www.xxxx.com对应的IP地址,此次dns解析圆满成功。

三、创建tcp链接
拿到域名对应的IP地址以后,User-Agent(通常是指浏览器)会以一个随机端口(1024< 端口 < 65535)向服务器的WEB程序(经常使用的有httpd,nginx等)80端口发起TCP的链接请求。这个链接请求(原始的http请求通过TCP/IP4层模型的层层封包)到达服务器端后(这中间经过各类路由设备,局域网内除外),进入到网卡,而后是进入到内核的TCP/IP协议栈(用于识别该链接请求,解封包,一层一层的剥开),还有可能要通过Netfilter防火墙(属于内核的模块)的过滤,最终到达WEB程序,最终创建了TCP/IP的链接。

tcp创建链接和关闭链接均须要一个完善的确认机制,咱们通常将链接称为三次握手,而链接关闭称为四次挥手。而不管是三次握手仍是四次挥手都须要数据从客户端到服务器的一次完整传输。将数据从客户端到服务端经历的一个完整时延包括:

  • 发送时延:把消息中的全部比特转移到链路中须要的时间,是消息长度和链路速度的函数
  • 传播时延:消息从发送端到接受端须要的时间,是信号传播距离和速度的函数
  • 处理时延:处理分组首部,检查位错误及肯定分组目标所需的时间

    • 排队时延:到来的分组排队等待处理的时间以上的延迟总和就是客户端到服务器的总延迟时间

以上的延迟总和就是客户端到服务器的总延迟时间。所以每一次的链接创建和断开都是有巨大代价的。所以去掉没必要要的资源和资源合并(包括js及css资源合并、雪碧图等)才会成为性能优化绕不开的方案。可是好消息是随着协议的发展咱们将对性能优化这个主题有着新的见解和思考。虽然还未到来,但也不远了。若是你感到好奇那就接着往下看。

如下简述下tcp创建链接的过程:
在这里插入图片描述
第一次握手:客户端发送syn包(syn=x,x为客户端随机序列号)的数据包到服务器,并进入SYN_SEND状态,等待服务器确认;
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=x+1),同时本身也发送一个SYN包(syn=y,y为服务端生成的随机序列号),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1)
此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。握手过程当中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。理想状态下,TCP链接一旦创建,在通讯双方中的任何一方主动关闭链接以前,TCP链接都将被一直保持下去

这里注意, 三次握手是不携带数据的,而是在握手完毕才开始数据传输。所以若是每次数据请求都须要从新进行完整的tcp链接创建,通讯时延的耗时是难以估量的!这也就是为何咱们老是能听到资源合并减小请求次数的缘由。


下面来看看HTTP如何在协议层面帮咱们进行优化的:

HTTP1.0
在http1.0时代,每一个TCP链接只能发送一个请求。发送数据完毕,链接就关闭,若是还要请求其余资源,就必须再新建一个链接。 TCP链接的新建成本很高,由于须要客户端和服务器三次握手,而且开始时发送速率较慢(TCP的拥塞控制开始时会启动慢启动算法)。在数据传输的开始只能发送少许包,并随着网络状态良好(无拥塞)指数增加。但遇到拥塞又要从新从1个包开始进行传输。

如下图为例,慢启动时第一次数据传输只能传输一组数据,获得确认后传输2组,每次翻倍,直到达到阈值16时开始启用拥塞避免算法,既每次获得确认后数据包只增长一个。当发生网络拥塞后,阈值减半从新开始慢启动算法。
在这里插入图片描述
所以为避免tcp链接的三次握手耗时及慢启动引发的发送速度慢的状况,应尽可能减小tcp链接的次数

而HTTP1.0每一个数据请求都须要从新创建链接的特色使得HTTP 1.0版本的性能比较差。随着网页加载的外部资源愈来愈多,这个问题就愈发突出了。 为了解决这个问题,有些浏览器在请求时,用了一个非标准的Connection字段。 Kepp-alive 一个能够复用的TCP链接就创建了,直到客户端或服务器主动关闭链接。可是,这不是标准字段,不一样实现的行为可能不一致,所以不是根本的解决办法。

HTTP1.1
http1.1(如下简称h1.1) 版的最大变化,就是引入了持久链接(persistent connection),即TCP链接默认不关闭,能够被多个请求复用,不用声明Connection: keep-alive。 客户端和服务器发现对方一段时间没有活动,就能够主动关闭链接。不过,规范的作法是,客户端在最后一个请求时,发送Connection: close,明确要求服务器关闭TCP链接。 目前,对于同一个域名,大多数浏览器容许同时创建6个持久链接。相比与http1.0,1.1的页面性能有了巨大提高,由于省去了不少tcp的握手挥手时间。下图第一种是tcp创建后只能发一个请求的http1.0的通讯状态,而拥有了持久链接的h1.1则避免了tcp握手及慢启动带来的漫长时延。
在这里插入图片描述
从图中能够看到相比h1.0,h1.1的性能有所提高。然而虽然1.1版容许复用TCP链接,可是同一个TCP链接里面,全部的数据通讯是按次序进行的。服务器只有处理完一个回应,才会进行下一个回应。要是前面的回应特别慢,后面就会有许多请求排队等着。这称为"队头堵塞"(Head-of-line blocking)。 为了不这个问题,只有三种方法:一是减小请求数,二是同时多开持久链接。这致使了不少的网页优化技巧,好比合并脚本和样式表、将图片嵌入CSS代码、域名分片(domain sharding)等等。若是HTTP协议能继续优化,这些额外的工做是能够避免的。三是开启pipelining,不过pipelining并非救世主,它也存在很多缺陷:

  • pipelining只能适用于http1.1,通常来讲,支持http1.1的server都要求支持pipelining。
  • 只有幂等的请求(GET,HEAD)能使用pipelining,非幂等请求好比POST不能使用,由于请求之间可能会存在前后依赖关系。
  • head of line blocking并无彻底获得解决,server的response仍是要求依次返回,遵循FIFO(first
    in first out)原则。也就是说若是请求1的response没有回来,2,3,4,5的response也不会被送回来。
  • 绝大部分的http代理服务器不支持pipelining。 和不支持pipelining的老服务器协商有问题。 可能会致使新的队首阻塞问题。

鉴于以上种种缘由,pipelining的支持度并不友好。能够看看chrome对pipelining的描述:https://www.chromium.org/deve...

在这里插入图片描述

HTTP2
2015年,HTTP/2 发布。它不叫 HTTP/2.0,是由于标准委员会不打算再发布子版本了,下一个新版本将是 HTTP/3。HTTP2将具备如下几个主要特色:

  • 二进制协议 :HTTP/1.1 版的头信息确定是文本(ASCII编码),数据体能够是文本,也能够是二进制。HTTP/2 则是一个完全的二进制协议,头信息和数据体都是二进制,而且统称为"帧"(frame):头信息帧和数据帧。
  • 多工 :HTTP/2 复用TCP链接,在一个链接里,客户端和浏览器均可以同时发送多个请求或回应,并且不用按照顺序一一对应,这样就避免了"队头堵塞"。
  • 数据流:由于 HTTP/2 的数据包是不按顺序发送的,同一个链接里面连续的数据包,可能属于不一样的回应。所以,必需要对数据包作标记,指出它属于哪一个回应。 HTTP/2 将每一个请求或回应的全部数据包,称为一个数据流(stream)。每一个数据流都有一个独一无二的编号。数据包发送的时候,都必须标记数据流ID,用来区分它属于哪一个数据流。另外还规定,客户端发出的数据流,ID一概为奇数,服务器发出的,ID为偶数。 数据流发送到一半的时候,客户端和服务器均可以发送信号(RST_STREAM帧),取消这个数据流。1.1版取消数据流的惟一方法,就是关闭TCP链接。这就是说,HTTP/2 能够取消某一次请求,同时保证TCP链接还打开着,能够被其余请求使用。 客户端还能够指定数据流的优先级。优先级越高,服务器就会越早回应。
  • 头信息压缩: HTTP 协议不带有状态,每次请求都必须附上全部信息。因此,请求的不少字段都是重复的,好比Cookie和User Agent,如出一辙的内容,每次请求都必须附带,这会浪费不少带宽,也影响速度。 HTTP2对这一点作了优化,引入了头信息压缩机制(header compression)。一方面,头信息使用gzip或compress压缩后再发送;另外一方面,客户端和服务器同时维护一张头信息表,全部字段都会存入这个表,生成一个索引号,之后就不发送一样字段了,只发送索引号,这样就提升速度了。
  • 服务器推送: HTTP/2 容许服务器未经请求,主动向客户端发送资源,这叫作服务器推送(server push)。 常见场景是客户端请求一个网页,这个网页里面包含不少静态资源。正常状况下,客户端必须收到网页后,解析HTML源码,发现有静态资源,再发出静态资源请求。其实,服务器能够预期到客户端请求网页后,极可能会再请求静态资源,因此就主动把这些静态资源随着网页一块儿发给客户端了

就这几个点咱们分别讨论一下:
就多工来看:虽然http1.1支持了pipelining,可是仍然会有队首阻塞问题,若是浏览器同时发出http请求请求和css,服务器端处理css请求耗时20ms,可是由于先请求资源是html,此时的css尽管已经处理好了但仍不能返回,而须要等待html处理好一块儿返回,此时的客户端就处于盲等状态,而事实上若是服务器先处理好css就先返回css的话,浏览器就能够开始解析css了。而多工的出现就解决了http以前版本协议的问题,极大的提高了页面性能。缩短了通讯时间。咱们来看看有了多工以后有那些影响:

  • 无需进行资源分片:为了不请求tcp链接耗时长的和初始发送速率低的问题,浏览器容许同时打开多个tcp链接让资源同时请求。可是为了不服务器压力,通常针对一个域名会有最大并发数的限制,通常来讲是6个。容许一个页面同时对相同域名打开6个tcp链接。为了绕过最大并发数的限制,会将资源分布在不一样的域名下,避免资源在超过并发数后须要等待才能开始请求。而有了http2,能够同步请求资源,资源分片这种方式就能够再也不使用。
  • 无需进行资源合并:资源合并会不利于缓存机制,由于单文件修改会影响整个资源包。并且单文件过大对于 HTTP/2 的传输很差,尽可能作到细粒化更有利于 HTTP/2 传输。并且内置资源也是同理,将资源以base64的形式放进代码中不利于缓存。且编码后的图片资源大小是要超过图片大小的。这二者都是以减小tcp请求次数增大单个文件大小来进行优化的。

就头部压缩来看:HTTP/1.1 版的头信息是ASCII编码,也就是不通过压缩的,当咱们请求只携带少许数据时,http头部可能要比载荷要大许多,尤为是有了很长的cookie以后这一点尤其显著,头部压缩毫无疑问能够对性能有很大提高。

就服务器推送来看:少去了资源请求的时间,服务端能够将可能用到的资源推送给服务端以待使用。这项能力几乎是革新了以前应答模式的认知,对性能提高也有巨大帮助。

所以不少优化都是在基于tcp及http的一些问题来避免和绕过的。事实上多数的优化都是针对网络通讯这个部分在作。

四、创建TCP链接后发起http请求

五、服务器端响应http请求,浏览器获得html代码


以上是网络通讯部分,接下来将会对页面渲染部分进行叙述。

  • 当浏览器拿到HTML文档时首先会进行HTML文档解析,构建DOM树。
  • 遇到css样式如link标签或者style标签时开始解析css,构建样式树。HTML解析构建和CSS的解析是相互独立的并不会形成冲突,所以咱们一般将css样式放在head中,让浏览器尽早解析css。
  • 当html的解析遇到script标签会怎样呢?答案是中止DOM树的解析开始下载js。由于js是会阻塞html解析的,是阻塞资源。其缘由在于js可能会改变html现有结构。例若有的节点是用js动态构建的,在这种状况下就会中止dom树的构建开始下载解析js。脚本在文档的何处插入,就在何处执行。当 HTML 解析器遇到一个 script 标记时,它会暂停构建 DOM,将控制权移交给 JavaScript 引擎;等 JavaScript 引擎运行完毕,浏览器会从中断的地方恢复 DOM 构建。而所以就会推迟页面首绘的时间。能够在首绘不须要js的状况下用async和defer实现异步加载。这样js就不会阻塞html的解析了。当HTML解析完成后,浏览器会将文档标注为交互状态,并开始解析那些处于“deferred”模式的脚本,也就是那些应在文档解析完成后才执行的脚本。而后,文档状态将设置为“完成”,一个“加载”事件将随之触发。
    注意,异步执行是指下载。执行js时仍然会阻塞。
  • 在获得DOM树和样式树后就能够进行渲染树的构建了。应注意的是渲染树和 DOM 元素相对应的,但并不是一一对应。好比非可视化的 DOM 元素不会插入呈现树中,例如“head”元素。若是元素的 display 属性值为“none”,那么也不会显示在呈现树中(可是 visibility 属性值为“hidden”的元素仍会显示)

这里写图片描述

  • 渲染树构建完毕后将会进行布局。布局使用流模型的Layout算法。所谓流模型,便是指Layout的过程只需进行一遍便可完成,后出如今流中的元素不会影响前出如今流中的元素,Layout过程只需从左至右从上至下一遍完成便可。但实际实现中,流模型会有例外。Layout是一个递归的过程,每一个节点都负责本身及其子节点的Layout。Layout结果是相对父节点的坐标和尺寸。其过程能够简述为:

    父节点肯定本身的宽度
     父节点完成子节点放置,肯定其相对坐标
     节点肯定本身的宽度和高度
     父节点根据全部的子节点高度计算本身的高度
  • 此时renderTree已经构建完毕,不过浏览器渲染树引擎并不直接使用渲染树进行绘制,为了方便处理定位(裁剪),溢出滚动(页内滚动),CSS转换/不透明/动画/滤镜,蒙版或反射,Z (Z排序)等,浏览器须要生成另一棵树 - 层树。所以绘制过程以下:

获取 DOM 并将其分割为多个层(RenderLayer)
将每一个层栅格化,并独立的绘制进位图中
将这些位图做为纹理上传至 GPU
复合多个层来生成最终的屏幕图像(终极layer)。


3、HTML及CSS样式的解析

HTML解析是一个将字节转化为字符,字符解析为标记,标记生成节点,节点构建树的过程。。CSS样式的解析则因为复杂的样式层叠而变得复杂。对此不一样的渲染引擎在处理上有所差别,后文将会就这点进行详细讲解

一、HTML的解析分为<font color='6ebc91'>标记化</font>和<font color='6ebc91'>树构建</font>两个阶段
标记化算法
是词法分析过程,将输入内容解析成多个标记。HTML标记包括起始标记、结束标记、属性名称和属性值。标记生成器识别标记,传递给树构造器,而后接受下一个字符以识别下一个标记;如此反复直到输入的结束。
该算法的输出结果是 HTML 标记。该算法使用状态机来表示。每个状态接收来自输入信息流的一个或多个字符,并根据这些字符更新下一个状态。当前的标记化状态和树结构状态会影响进入下一状态的决定。这意味着,即便接收的字符相同,对于下一个正确的状态也会产生不一样的结果,具体取决于当前的状态。
树构建算法
在树构建阶段,以 Document 为根节点的 DOM 树也会不断进行修改,向其中添加各类元素。
标记生成器发送的每一个节点都会由树构建器进行处理。规范中定义了每一个标记所对应的 DOM 元素,这些元素会在接收到相应的标记时建立。这些元素不只会添加到 DOM 树中,还会添加到开放元素的堆栈中。此堆栈用于纠正嵌套错误和处理未关闭的标记。其算法也能够用状态机来描述。这些状态称为“插入模式”。

如下将会举一个例子来分析这两个阶段:

<html>
  <body>
    Hello world
  </body>
</html>

标记化:初始状态是数据状态。

  • 遇到字符 < 时,状态更改成“标记打开状态”。接收一个a-z字符会建立“起始标记”,状态更改成“标记名称状态”。这个状态会一直保持到接收>字符。在此期间接收的每一个字符都会附加到新的标记名称上。在本例中,咱们建立的标记是 html 标记。

  

  • 遇到 > 标记时,会发送当前的标记,状态改回“数据状态”。<body> 标记也会进行一样的处理。目前 html 和 body 标记均已发出。如今咱们回到“数据状态”。接收到 Hello world 中的 H 字符时,将建立并发送字符标记,直到接收 </body>中的<。咱们将为 Hello world 中的每一个字符都发送一个字符标记。

  

  • 如今咱们回到“标记打开状态”。接收下一个输入字符 / 时,会建立 end tag token
    并改成“标记名称状态”。咱们会再次保持这个状态,直到接收 >。而后将发送新的标记,并回到“数据状态”。</html> 输入也会进行一样的处理。

仍是以上的例子,咱们来看看树构建
树构建: 树构建阶段的输入是一个来自标记化阶段的标记序列。

  • 第一个模式是“initial mode”。接收 HTML 标记后转为“before html”模式,并在这个模式下从新处理此标记。这样会建立一个 HTMLHtmlElement 元素,并将其附加到 Document根对象上。

  

  • 而后状态将改成“before head”。此时咱们接收“body”标记。即便咱们的示例中没有“head”标记,系统也会隐式建立一个 HTMLHeadElement,并将其添加到树中。

  

  • 如今咱们进入了“in head”模式,而后转入“after head”模式。系统对 body 标记进行从新处理,建立并插入
    HTMLBodyElement,同时模式转变为“body”。

  

  • 如今,接收由“Hello world”字符串生成的一系列字符标记。接收第一个字符时会建立并插入“Text”节点,而其余字符也将附加到该节点。

  

  • 接收 body 结束标记会触发“after body”模式。如今咱们将接收 HTML 结束标记,而后进入“after after
    body”模式。接收到文件结束标记后,解析过程就此结束。 解析结束后的操做

在此阶段,浏览器会将文档标注为交互状态,并开始解析那些处于“deferred”模式的脚本,也就是那些应在文档解析完成后才执行的脚本。而后,文档状态将设置为“完成”,一个“加载”事件将随之触发。

完整解析过程以下图:
这里写图片描述

二、CSS的解析与层叠规则
每个呈现器都表明了一个矩形的区域,一般对应于相关节点的 CSS 框,这一点在 CSS2 规范中有所描述。它包含诸如宽度、高度和位置等几何信息。就是咱们 CSS 里常提到的盒子模型。构建呈现树时,须要计算每个呈现对象的可视化属性。这是经过计算每一个元素的样式属性来完成的。因为应用规则涉及到至关复杂的层叠规则,因此给样式树的构建形成了巨大的困难。为何说它复杂?由于同一个元素可能涉及多条样式,就须要判断最终到底哪条样式生效。首先咱们来了解一下css的样式层叠规则

①层叠规则:
根据不一样的样式来源优先级排列从小到大:
1>、用户端声明:来自浏览器的样式,被称做 UA style,是浏览器默认的样式。 好比,对于 DIV 元素,浏览器默认其 'display' 的特性值是 "block",而 SPAN 是 "inline"。
2>、通常用户声明:这个样式表是使用浏览器的用户,根据本身的偏好设置的样式表。好比,用户但愿全部 P 元素中的字体都默认显示成蓝色,能够先定义一个样式表,存成 css 文件。
3>、通常做者声明:即开发者在开发网页时,所定义的样式表。
4>、加了'!important' 的做者声明
5>、加了'!important' 的用户声明
!important 规则1:根据 CSS2.1 规范中的描述,'!important' 能够提升样式的优先级,它对样式优先级的影响是巨大的。 关于important在css2.1中的定义请点击这里
注意,'!important' 规则在 IE7 之前的版本中是被支持不完善。所以,常常被用做 CSS hack2。

若是来源和重要性相同则根据CSS specificity来进行断定。

特殊性的值能够看做是一个由四个数组成的一个组合,用 a,b,c,d 来表示它的四个位置。 依次比较 a,b,c,d 这个四个数比较其特殊性的大小。 好比,a 值相同,那么 b 值大的组合特殊性会较大,以此类推。 注意,W3C 中并非把它做为一个 4 位数来看待的。
a,b,c,d 值的肯定规则:

  • 若是 HTML 标签的 'style' 属性中该样式存在,则记 a 为 1;
  • 数一下选择器中 ID 选择器的个数做为 b 的值。好比,样式中包含 '#c1' 和 '#c2' 的选择器;
  • 其余属性以及伪类(pseudo-classes)的总数量是 c 的值。好比'.con',':hover' 等;
  • 元素名和伪元素的数量是 d 的值

在这里咱们来看一个W3C给出的例子:

*             {}  /* a=0 b=0 c=0 d=0 -> specificity = 0,0,0,0 */
li            {}  /* a=0 b=0 c=0 d=1 -> specificity = 0,0,0,1 */
li:first-line {}  /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
ul li         {}  /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
ul ol+li      {}  /* a=0 b=0 c=0 d=3 -> specificity = 0,0,0,3 */
h1 + *[rel=up]{}  /* a=0 b=0 c=1 d=1 -> specificity = 0,0,1,1 */
ul ol li.red  {}  /* a=0 b=0 c=1 d=3 -> specificity = 0,0,1,3 */
li.red.level  {}  /* a=0 b=0 c=2 d=1 -> specificity = 0,0,2,1 */
#x34y         {}  /* a=0 b=1 c=0 d=0 -> specificity = 0,1,0,0 */
style=""          /* a=1 b=0 c=0 d=0 -> specificity = 1,0,0,0 */

那么在以下例子中字体的显示应当为绿色:

<head>
<style type="text/css">
 #box { color: red }
</style>
</head>
<body>
<p id="box" style="color: green">
</body>

总结为表格的话计算规则以下:
这里写图片描述

②CSS解析
为了简化样式计算,Firefox 还采用了另外两种树:规则树和样式上下文树。Webkit 也有样式对象,但它们不是保存在相似样式上下文树这样的树结构中,只是由 DOM 节点指向此类对象的相关样式。

1>、Firefox的规则树和样式上下文树:
样式上下文包含端值。要计算出这些值,应按照正确顺序应用全部的匹配规则,并将其从逻辑值转化为具体的值。例如,若是逻辑值是屏幕大小的百分比,则须要换算成绝对的单位。规则树的点子真的很巧妙,它使得节点之间能够共享这些值,以免重复计算,还能够节约空间。
全部匹配的规则都存储在树中。路径中的底层节点拥有较高的优先级。规则树包含了全部已知规则匹配的路径。规则的存储是延迟进行的。规则树不会在开始的时候就为全部的节点进行计算,而是只有当某个节点样式须要进行计算时,才会向规则树添加计算的路径。
这个想法至关于将规则树路径视为词典中的单词。若是咱们已经计算出以下的规则树:
这里写图片描述
假设咱们须要为内容树中的另外一个元素匹配规则,而且找到匹配路径是 B - E - I(按照此顺序)。因为咱们在树中已经计算出了路径 A - B - E - I - L,所以就已经有了此路径,这就减小了如今所需的工做量。

那么Firefox是如何解决样式计算难题的呢?接下来看一个样例,假设咱们有以下HTML代码:

<html>
  <body>
     <div class="err" id="div1">
        <p>
        this is a <span class="big"> big error </span>
        this is also a
        <span class="big"> very  big  error</span> error
        </p>
     </div>
     <div class="err" id="div2">another error</div>
  </body>
</html>

而且咱们有以下规则:

div {margin:5px;color:black}
.err {color:red}
.big {margin-top:3px}
div span {margin-bottom:4px}
#div1 {color:blue}
#div2 {color:green}

为了简便起见,咱们只须要填充两个结构:color 结构和 margin 结构。color 结构只包含一个成员(即“color”),而 margin 结构包含四条边。
造成的规则树以下图所示(节点的标记方式为“节点名 : 指向的规则序号”):

这里写图片描述

上下文树以下图所示(节点名 : 指向的规则节点):

这里写图片描述

假设咱们解析 HTML 时遇到了第二个 <div> 标记,咱们须要为此节点建立样式上下文,并填充其样式结构。
通过规则匹配,咱们发现该 <div> 的匹配规则是第 一、2 和 6 条。这意味着规则树中已有一条路径可供咱们的元素使用,咱们只须要再为其添加一个节点以匹配第 6 条规则(规则树中的 F 节点)。
咱们将建立样式上下文并将其放入上下文树中。新的样式上下文将指向规则树中的 F 节点。
如今咱们须要填充样式结构。首先要填充的是 margin 结构。因为最后的规则节点 (F) 并无添加到 margin 结构,咱们须要上溯规则树,直至找到在先前节点插入中计算过的缓存结构,而后使用该结构。咱们会在指定 margin 规则的最上层节点(即 B 节点)上找到该结构。
咱们已经有了 color 结构的定义,所以不能使用缓存的结构。因为 color 有一个属性,咱们无需上溯规则树以填充其余属性。咱们将计算端值(将字符串转化为 RGB 等)并在此节点上缓存通过计算的结构。
第二个 <span> 元素处理起来更加简单。咱们将匹配规则,最终发现它和以前的 span 同样指向规则 G。因为咱们找到了指向同一节点的同级,就能够共享整个样式上下文了,只需指向以前 span 的上下文便可。
对于包含了继承自父代的规则的结构,缓存是在上下文树中进行的(事实上 color 属性是继承的,可是 Firefox 将其视为 reset 属性,并缓存到规则树上)。
例如,若是咱们在某个段落中添加 font 规则:

p {font-family:Verdana;font size:10px;font-weight:bold}

那么,该段落元素做为上下文树中的 div 的子代,就会共享与其父代相同的 font 结构(前提是该段落没有指定 font 规则)。

2>、Webkit的样式解析
在 Webkit 中没有规则树,所以会对匹配的声明遍历 4 次。首先应用非重要高优先级的属性(因为做为其余属性的依据而应首先应用的属性,例如 display),接着是高优先级重要规则,而后是普通优先级非重要规则,最后是普通优先级重要规则。这意味着屡次出现的属性会根据正确的层叠顺序进行解析。最后出现的最终生效。


4、渲染树的构建

样式树和DOM树链接在一块儿造成一个渲染树,渲染树用来计算可见元素的布局而且做为将像素渲染到屏幕上的过程的输入。值得一提的是,Gecko 将视觉格式化元素组成的树称为“框架树”。每一个元素都是一个框架。Webkit 使用的术语是“渲染树”,它由“呈现对象”组成。 Webkit 和 Gecko 使用的术语略有不一样,但总体流程是基本相同的。

接下来未来看一下两种渲染引擎的工做流程:
Webkit 主流程:
这里写图片描述

Mozilla 的 Gecko 呈现引擎主流程
这里写图片描述

虽然 Webkit 和 Gecko 使用的术语略有不一样,但总体流程是基本相同的。

Gecko 将视觉格式化元素组成的树称为“框架树”。每一个元素都是一个框架。
Webkit 使用的术语是“呈现树”,它由“呈现对象”组成。
对于元素的放置,Webkit 使用的术语是“布局”,而 Gecko 称之为“重排”。
对于链接 DOM 节点和可视化信息从而建立呈现树的过程,Webkit 使用的术语是“附加”。有一个细微的非语义差异,就是 Gecko 在 HTML 与 DOM 树之间还有一个称为“内容槽”的层,用于生成 DOM 元素。咱们会逐一论述流程中的每一部分。


5、关于浏览器渲染过程当中须要了解的概念

Repaint(重绘)——屏幕的一部分要重画,好比某个CSS的背景色变了。可是元素的几何尺寸没有变。
Reflow(重排)——意味着元件的几何尺寸变了,咱们须要从新验证并计算Render Tree。是Render Tree的一部分或所有发生了变化。这就是Reflow,或是Layout。reflow 会从<html>这个root frame开始递归往下,依次计算全部的结点几何尺寸和位置,在reflow过程当中,可能会增长一些frame,好比一个文本字符串必需被包装起来。
onload事件——当 onload 事件触发时,页面上全部的DOM,样式表,脚本,图片,flash都已经加载完成了。
DOMContentLoaded 事件——当 DOMContentLoaded 事件触发时,仅当DOM加载完成,不包括样式表,图片,flash。
首屏时间——当浏览器显示第一屏页面所消耗的时间,在国内的网络条件下,一般一个网站,若是“首屏时间”在2秒之内是比较优秀的,5秒之内用户能够接受,10秒以上就不可容忍了。
白屏时间——指浏览器开始显示内容的时间。可是在传统的采集方式里,是在HTML的头部标签结尾里记录时间戳,来计算白屏时间。在这个时刻,浏览器开始解析身体标签内的内容。而现代浏览器不会等待CSS树(全部CSS文件下载和解析完成)和DOM树(整个身体标签解析完成)构建完成才开始绘制,而是立刻开始显示中间结果。因此常常在低网速的环境中,观察到页面由上至下缓慢显示完,或者先显示文本内容后再重绘成带有格式的页面内容。


6、页面优化方案

本文的主题在于从浏览器的渲染过程谈页面优化。了解浏览器如何通讯并将拿到的数据如何进行解析渲染,本节将从网络通讯、页面渲染、资源预取及如何除了以上方案外,如何借助chrome来针对一个页面进行实战优化四个方面来谈。

从网络通讯过程入手能够作的优化

减小DNS查找
每一次主机名解析都须要一次网络往返,从而增长请求的延迟时间,同时还会阻塞后续请求。

重用TCP链接
尽量使用持久链接,以消除 TCP 握手和慢启动延迟;

减小HTTP重定向
HTTP 重定向极费时间,特别是不一样域名之间的重定向,更加费时;这里面既有额外的 DNS 查询、TCP 握手,还有其余延迟。最佳的重定向次数为零。

使用 CDN(内容分发网络)
把数据放到离用户地理位置更近的地方,能够显著减小每次 TCP 链接的网络延迟,增大吞吐量。

去掉没必要要的资源
任何请求都不如没有请求快。说到这,全部建议都无需解释。延迟是瓶颈,最快的速度莫过于什么也不传输。然而,HTTP 也提供了不少额外的机制,好比缓存和压缩,还有与其版本对应的一些性能技巧。

在客户端缓存资源
应该缓存应用资源,从而避免每次请求都发送相同的内容。(浏览器缓存)

传输压缩过的内容
传输前应该压缩应用资源,把要传输的字节减至最少:确保每种要传输的资源采用最好的压缩手段。(Gzip,减小60%~80%的文件大小)

消除没必要要的请求开销
减小请求的 HTTP 首部数据(好比HTTPcookie),节省的时间至关于几回往返的延迟时间。

并行处理请求和响应
请求和响应的排队都会致使延迟,不管是客户端仍是服务器端。这一点常常被忽视,但却会无谓地致使很长延迟。

针对协议版本采起优化措施
HTTP 1.x 支持有限的并行机制,要求打包资源、跨域分散资源,等等。相对而言,
HTTP 2.0 只要创建一个链接就能实现最优性能,同时无需针对 HTTP 1.x 的那些优化方法。
可是压缩、使用缓存、减小dns等的优化方案不管在哪一个版本都一样适用


你须要了解的资源预取

preload :能够对当前页面所需的脚本、样式等资源进行预加载,而无需等到解析到 script 和 link 标签时才进行加载。这一机制使得资源能够更早的获得加载并可用,且更不易阻塞页面的初步渲染,进而提高性能。
用法文档:https://developer.mozilla.org...

prefetch:prefetch 和 preload 同样,都是对资源进行预加载,可是 prefetch 通常预加载的是其余页面会用到的资源。 固然,prefetch 不会像 preload 同样,在页面渲染的时候加载资源,而是利用浏览器空闲时间来下载。当进入下一页面,就可直接从 disk cache 里面取,既不影响当前页面的渲染,又提升了其余页面加载渲染的速度。
用法文档:https://developer.mozilla.org...

subresource: <link rel="subresource">被Chrome支持了有一段时间,而且已经有些搔到预加载当前导航/页面(所含有的资源)的痒处了。但它有一个问题——没有办法处理所获取内容的优先级(as也并不存在),因此最终,这些资源会以一个至关低的优先级被加载,这使得它能提供的帮助至关有限

prerender:prerender 就像是在后台打开了一个隐藏的 tab,会下载全部的资源、建立DOM、渲染页面、执行js等等。若是用户进入指定的连接,隐藏的这个页面就会立马进入用户的视线。 可是要注意,必定要在十分肯定用户会点击某个连接时才使用该特性,不然客户端会无故的下载不少资源和渲染这个页面。 正如任何提早动做同样,预判老是有必定风险出错。若是提早的动做是昂贵的(好比高CPU、耗电、占用带宽),就要谨慎使用了。

preconnect: preconnect 容许浏览器在一个 HTTP 请求正式发给服务器前预先执行一些操做,这包括

dns-prefetch:经过 DNS 预解析来告诉浏览器将来咱们可能从某个特定的 URL 获取资源,当浏览器真正使用到该域中的某个资源时就能够尽快地完成 DNS 解析

这些属性虽然并不是全部浏览器都支持,可是不支持的浏览器也只是不处理而已,而是别的话则会省去不少时间。所以,合理的使用资源预取能够显著提升页面性能。


高效合理的css选择符能够减轻浏览器的解析负担。

由于css是逆向解析的因此应当避免多层嵌套。

  • 避免使用通配规则。如 *{} 计算次数惊人!只对须要用到的元素进行选择
  • 尽可能少的去对标签进行选择,而是用class。如:#nav li{},能够为li加上nav_item的类名,以下选择.nav_item{}
  • 不要去用标签限定ID或者类选择符。如:ul#nav,应该简化为#nav
  • 尽可能少的去使用后代选择器,下降选择器的权重值。后代选择器的开销是最高的,尽可能将选择器的深度降到最低,最高不要超过三层,更多的使用类来关联每个标签元素。
  • 考虑继承。了解哪些属性是能够经过继承而来的,而后避免对这些属性重复指定规则

从js层面谈页面优化

①解决渲染阻塞
若是在解析HTML标记时,浏览器遇到了JavaScript,解析会中止。只有在该脚本执行完毕后,HTML渲染才会继续进行。因此这阻塞了页面的渲染。
解决方法:在标签中使用 async或defer特性
②减小对DOM的操做
对DOM操做的代价是高昂的,这在网页应用中的一般是一个性能瓶颈。
解决办法:修改和访问DOM元素会形成页面的Repaint和Reflow,循环对DOM操做更是罪恶的行为。因此请合理的使用JavaScript变量储存内容,考虑大量DOM元素中循环的性能开销,在循环结束时一次性写入。
减小对DOM元素的查询和修改,查询时可将其赋值给局部变量。
③使用JSON格式来进行数据交换
JSON是一种轻量级的数据交换格式,采用彻底独立于语言的文本格式,是理想的数据交换格式。同时,JSON是 JavaScript原生格式,这意味着在 JavaScript 中处理 JSON数据不须要任何特殊的 API 或工具包。
④让须要常常改动的节点脱离文档流
由于重绘有时确实不可避免,因此只能尽量限制重绘的影响范围。


如何借助chrome针对性优化页面

首先打开控制台,点击Audits一栏,会看到以下表单。在选取本身须要模拟测试的状况后点击run audits,便可开始页面性能分析。
在这里插入图片描述

而后将会获得分析结果及优化建议:

咱们能够逐项根据现有问题进行优化,如性能类目(performance)中的第一项优化建议延迟加载屏幕外图像(defer offscreen images),点击后就能看到详情如下详情:
在这里插入图片描述

而具体页面的指标优化能够根据给出的建议进行逐条优化。目前提供的性能分析及建议的列表包括性能分析、渐进式web应用、最佳实践、无障碍访问及搜索引擎优化五个部分。基本上涵盖了常见优化方案及性能点的方方面面,开发时合理使用也能更好的提高页面性能


相信以上优化方案之因此行之有效的缘由大均可以在本文中找出缘由。理论是用来指导实践的,即不能闭门造车式的埋头苦干,也不能绝不实践的夸夸其谈。这样才会造成完整的知识体系,让知识体系树更加庞大。知道该如何优化是一回事,真正合理应用是另外一回事,要有好的性能,要着手于能作的每一件“小事”。


7、附录

性能优化是一门艺术,更是一门综合艺术。这其中涉及不少知识点。而这些知识点都有不少不错的文章进行了总结。若是你想深刻探究或许这里推荐的文章会给你启发。

HTTP2详解:https://www.jianshu.com/p/e57...
TCP拥塞控制: https://www.cnblogs.com/losby...
页面性能分析网站:https://gtmetrix.com/analyze....
Timing官方文档: https://www.w3.org/TR/navigat...
chrome中的高性能网络 https://www.cnblogs.com/xuan5...

相关文章
相关标签/搜索