CSS渲染原理,优化策略

江湖大侠 -
CSS渲染原理,优化策略

一、css渲染原理(Cascading Style Sheets)
在介绍css渲染原理之前,我们简明扼要介绍一下页面的加载过程和浏览器渲染过程,有助于更好理解后续css渲染原理。
1.1页面的加载过程和浏览器渲染过程
1.1.1页面加载过程
要点如下:

浏览器根据DNS服务器得到域名的IP地址
向这个IP的机器发送HTTP请求
服务器收到、处理并返回HTTP请求
浏览器得到返回内容(其实就是一堆HTML格式的字符串,因为只有HTML格式浏览器才能正确解析,这是W3C标准的要求。接下来就是浏览器的渲染过程。)
1.1.2 浏览器渲染过程
第一步:html经过HTML parser解析为DOM tree;(浏览器会把HTML结构字符串解析转换DOM树形结构。)
第二步:css根据css规则经过css解析器解析为 style Rules(CSSOM tree);
第三步:两棵树经过attachment结合Render Tree(形成一颗大树,此时它还是一颗迷茫的树,不知道节点的内容和位置)
第四步:render tree (渲染树)经过Layout计算DOM的位置以及样式;
第五步: 讲计算好的页面paint画出来;
第六步:显示到浏览器上;
流程图:

简而言之,浏览器就是解析DOM生成DOM Tree,结合CSS生成CSS Tree,最终组成render tree,再渲染页面。(构建DOM->构建CSSOM->构建渲染树->布局->绘制)
1.1.3影响DOM树构建的因素(js和css)
先做个总结,然后再进行具体的分析:
CSS不会阻塞DOM的解析,但是会影响JAVAScript的运行,javaSscript会阻止DOM树的解析,最终css(CSSOM)会影响DOM树的渲染,也可以说最终会影响渲染树的生成。
接下来我们先看javascript对DOM树构建和渲染是如何造成影响的,分成三种类型来讲解:
1、JavaScript脚本再html页面中
<html>
<body>
<div>1</div>
<script>
let div1 = document.getElementsByTagName('div')[0]
div1.innerText = 'time.geekbang'
</script>
<div>test</div>
</body>
</html>

我在两段div中间插入了一段JavaScript脚本,这段脚本的解析过程就有点不一样了。
通过前面DOM生成流程分析,我们已经知道当解析到script脚本标签时,其DOM树结构如下所示:

这时候HTML解析器暂停工作,javascript引擎介入,并执行script标签中的这段脚本,因为这段javascript脚本修改了DOM中第一个div中的内容,所以执行这段脚本之后,div节点内容已经修改为time.geekbang了。脚本执行完成之后,HTML解析器回复解析过程,继续解析后续的内容,直至生成最终的DOM。
以上过程应该还是比较好理解的,不过除了页面中直接内嵌JavaScript脚本之外,我们还通常需要在页面中引入JAVAScript文件,这个解析过程就稍微复杂了些,如下面案例:
2、html页面中引入javaScript文件

//foo.js
let div1 = document.getElementsByTagName('div')[0]
div1.innerText = 'time.geekbang'

<html>
<body>
<div>1</div>
<script type="text/javascript" src='foo.js'></script>
<div>test</div>
</body>
</html>

这段代码的功能还是和前面那段代码是一样的,不过这里我把内嵌JavaScript脚本修改成了通过javaScript文件加载。其整个执行流程还是一样的,执行到JAVAScript标签时,暂停整个DOM的解析,执行javascript代码,不过这里执行javascript时,需要现在在这段代码。这里需要重点关注下载环境,因为javascript文件的下载过程会阻塞DOM解析,而通常下载又是非常耗时的,会受到网络环境、javascript文件大小等因素的影响。
优化机制:
不过谷歌浏览器做了很多优化,其中一个主要的优化就是预解析操作。当渲染引擎收到字节流之后,会开启一个预解析线程,用来分析HTML文件中包含的JavaScript、CSS等相关文件,解析到相关文件之后,会开启一个预解析线程,用来分析HTML文件中包含的javascprit、css等相关文件、解析到相关文件之后,预解析线程会提前下载这些文件。前端培训
再回到DOM解析上,我们知道引入javascprit线程会阻塞DOM,不过也有一些相关的策略来规避,比如使用CDN来加速Java
Script文件的家在速度,压缩javascript、CSS等相关文件,解析到相关文件之后,预解析线程会提前下载这些文件。
再回到 DOM 解析上,我们知道引入 JavaScript 线程会阻塞 DOM,不过也有一些相关的策略来规避,比如使用 CDN 来加速 JavaScript 文件的加载,压缩 JavaScript 文件的体积。另外,如果 JavaScript 文件中没有操作 DOM 相关代码,就可以将该 JavaScript 脚本设置为异步加载,通过 async 或 defer 来标记代码,使用方式如下所示:

<script async type="text/javascript" src='foo.js'></script>
或者
<script defer type="text/javascript" src='foo.js'></script>

async和defer区别:
async:脚本并行加载,加载完成之后立即执行,执行时机不确定,仍有可能阻塞HTML解析,执行时机在load事件派发之前。
defer:脚本并行加载,等待HTML解析完成之后,按照加载顺序执行脚本,执行时机DOMContentLoaded事件派发之前。
3、html页面中有css样式
案例:

//theme.css
div {color:blue}

<html>

<head>
    <style src='theme.css'></style>
</head>

<body>

<div>1</div>
<script>
    let div1 = document.getElementsByTagName('div')[0]
    div1.innerText = 'time.geekbang' // 需要 DOM
    div1.style.color = 'red' // 需要 CSSOM
</script>
<div>test</div>

</body>
</html>

该示例中,JavaScript 代码出现了 div1.style.color = ‘red’ 的语句,它是用来操纵 CSSOM 的,所以在执行 JavaScript 之前,需要先解析 JavaScript 语句之上所有的CSS 样式。所以如果代码里引用了外部的 CSS 文件,那么在执行 JavaScript 之前,还需要等待外部的 CSS 文件下载完成,并解析生成 CSSOM 对象之后,才能执行 JavaScript 脚本。
而 JavaScript 引擎在解析 JavaScript 之前,是不知道 JavaScript 是否操纵了 CSSOM的,所以渲染引擎在遇到 JavaScript 脚本时,不管该脚本是否操纵了 CSSOM,都会执行CSS 文件下载,解析操作,再执行 JavaScript 脚本。所以说 JavaScript 脚本是依赖样式表的,这又多了一个阻塞过程。
总结:
通过上面三点的分析,我们知道了 JavaScript 会阻塞 DOM 生成,而样式文件又会阻塞js的执行。
4、接下来我们再看CSS对DOM树构建和渲染的影响:(假设法)
假设css不影响DOM树的解析,这个时候你加载css的时候,很可能会修改下面DOM节点的样式,
如果css加载不阻塞render树渲染的话,那么当css加载完之后,render树可能又得重新重绘或者回流了,这就造成了一些没有必要的损耗。所以这个假设是不成立得。
所以我们得出:css加载不会阻塞DOM树的解析,但会阻塞render树的渲染(渲染时需等CSS加载完毕)

因此,从上面js和css对DOM树构建的分析得出,应该把CSS放在文档的头部,尽可能的提前加载CSS;把JS放在文档的尾部,这样JS也不会阻塞页面的渲染。CSS会和JS并行解析,CSS解析也尽可能的不去阻塞JS的执行,从而使页面尽快的渲染完成。
1.3 CSS渲染规则
css渲染规则,是从上到下,从右到左渲染的。这是为什么呢?举个例子:

<div>
<div >

  <p><span> 111 </span></p>
  <p><span> 222 </span></p>
  <p><span> 333 </span></p>
  <p><span class='yellow'> 444 </span></p>

</div>
</div>

<style>
div > div.jartto p span.yellow {
color: yellow;
}
</style>

我们按照「从左到右」的方式进行分析:

先找到所有 div 节点。
在 div 节点内找到所有的子 div,并且是 class = “jac”。
然后再依次匹配 p span.yellow 等情况。
遇到不匹配的情况,就必须回溯到一开始搜索的 div 或者 p 节点,然后去搜索下个节点,重复这样的过程。
综上:这样的搜索过程对于一个只是匹配很少节点的选择器来说,效率是极低的,因为我们花费了大量的时间在回溯匹配不符合规则的节点。
我们按照「从右向左」的方式进行分析:
1.首先就查找到 class=“yellow” 的 span 元素。
2 .接着检测父节点是否为 p 元素,如果不是则进入同级其他节点的遍历,如果是则继续匹配父节点满足 class=“jac” 的 div 容器。
3.这样就又减少了集合的元素,只有符合当前的子规则才会匹配再上一条子规则。
综上所述,我们可以得出结论:
浏览器 CSS 匹配核心算法的规则是以从右向左方式匹配节点的,这样做是为了减少无效匹配次数,从而匹配快、性能更优。
1.4 CSS选择器权值(优先级)
示例代码一:
<div >
<p id="box" >Jartto's blog</p>
</div>
<style>
#box{color: red;}
.text{color: yellow;}
</style>

当你知道 「ID 选择器 > 类选择器 」的时候,答案不言自明。
示例代码二:

<div id="box">
<p >Jartto's blog</p>
</div>
<style>
#box{color: red;}
.text{color: blue;}
</style>

这里就考查到了规则「类选择器 > 继承」,ID 对文本来说是继承过来的属性,所以优先级不如直接作用在元素上面的类选择器。
!important > 行内样式(权重1000) > ID 选择器(权重 100) > 类选择器(权重 10) > 标签(权重1) > 通配符 > 继承 > 浏览器默认属性
权值,代表优先级,权值越大,优先级越高。同种类型的选择器权值相同,后定义的选择器会覆盖先定义的选择器。注:组合使用,权值会叠加
二、优化策略
对于绝大部分的开发者来说,css的功能就是完成页面布局,制定页面的展示效果。其实css也有许多实现Web性能优化的方法。
我们都知道对于网站来说,性能至关重要,CSS作为页面渲染和内容展现的重要环节,影响着用户对整个网站的第一体验。因此,与其相关的性能优化是不容忽视的。
2.1 CSS 书写顺序对性能有影响吗?
需要注意的是:浏览器并不是一获取到 CSS 样式就立马开始解析,而是根据 CSS 样式的书写顺序将之按照 DOM 树的结构分布渲染样式,然后开始遍历每个树结点的 CSS 样式进行解析,此时的 CSS 样式的遍历顺序完全是按照之前的书写顺序。
在解析过程中,一旦浏览器发现某个元素的定位变化影响布局,则需要倒回去重新渲染。
栗子:

width: 150px;
height: 150px;
font-size: 24px;
position: absolute;

当浏览器解析到 position 的时候突然发现该元素是绝对定位元素需要脱离文档流,而之前却是按照普通元素进行解析的,所以不得不重新渲染。
渲染引擎首先解除该元素在文档中所占位置,这就导致了该元素的占位情况发生了变化,其他元素可能会受到它回流的影响而重新排位。
我们对代码进行调整:

position: absolute;
width: 150px;
height: 150px;
font-size: 24px;

在实际开发过程中,我们如何能保证自己的书写顺序是最优呢?
这里有一个规范,建议顺序大致如下:

定位属性position display float left top right bottom overflow clear z-index
自身属性width height padding border margin background
文字样式font-family font-size font-style font-weight font-varient color
文本属性text-align vertical-align text-wrap text-transform text-indent text-decoration letter-spacing word-spacing white-space text-overflow
CSS3 中新增属性content box-shadow border-radius transform
总之,我们需要知道这个规则就够了,剩下的可以交给一些插件去做,譬如 CSSLint
2.2 有选择地使用选择器
大家都知道css选择器的匹配是从右向左进行的,这一策略导致不同种类的选择器之间的性能也存在差异。
举个例子:
相比于#markdown-content-h3,显然使用#markdown .content h3时,浏览器生成渲染树(render-tree)所要花费的时间更多。因为后者需要先找到DOM中的所有h3元素,再过滤掉祖先元素不是.content的,最后过滤掉.content的祖先不是#markdown的。试想,如果嵌套的层级更多,页面中的元素更多,那么匹配所要花费的时间代价自然更高。
不过现代浏览器在这一方面做了很多优化,不同选择器的性能差别并不明显,甚至可以说差别甚微。此外不同选择器在不同浏览器中的性能表现8也不完全统一,在编写CSS的时候无法兼顾每种浏览器。鉴于这两点原因,我们在使用选择器时,只需要记住以下几点即可。
1、保持简单,不要使用嵌套过多过于复杂的选择器。例:
/ Bad /
div > div > div > p {color:red;}
/ Good /
p-class{color:red;}

2、通配符和属性选择器效率最低,需要匹配的元素最多,尽量避免使用。
只用通配符设定一切基础的样式,如:* {margin:0; padding:0;}这种方式,代码少,但是性能差,因为渲染的时候,要匹配页面上所有的元素!很多基础样式没有margin和padding的元素,比如div,li等。都被匹配,完全没必要!
3、不要在id选择器前使用标签名,id选择器本身就能唯一确定一个元素,没必要再在前面加上标签名,这样多此一举,还会降低效率。
如:div #box { color: white; } div 可不加
4、不要为了追求速度而放弃可读性与可维护性。
2.3 把stylesheets放在HTML页面头部
浏览器所有的stylesheets加载完成之后,才会开始渲染整个页面。在这之前,浏览器不会渲染页面的任何内容,页面会一直呈现空白。(因为要把stylesheets放在头部的原因)
如果放在HTML页面底部,页面渲染就不仅仅实在等在stylesheets的加载,还要等待html内容的加载完成,这样导致用户看到页面的时间会更晚。
2.4 减少使用昂贵的属性
在浏览器绘制屏幕时,所有需要浏览器进行操作或计算的属性相对而言都需要花费更大的代价。当页面发生重绘时,它们会降低浏览器的渲染性能。所以在编写CSS时,我们应该尽量减少使用昂贵属性,如box-shadow/border-radius/filter/透明度/:nth-child等。
当然,并不是让大家不要使用这些属性,因为这些应该都是我们经常使用的属性。之所以提这一点,是让大家对此有一个了解。当有两种方案可以选择的时候,可以优先选择没有昂贵属性或昂贵属性更少的方案,如果每次都这样的选择,网站的性能会在不知不觉中得到一定的提升。
2.5 避免使用@important命令
不建议使用@import主要有以下两点原因。
首先,使用@import引入CSS会影响浏览器的并行下载。使用@import引用的CSS文件只有在引用它的那个css文件被下载、解析之后,浏览器才会知道还有另外一个css需要下载,这时才去下载,然后下载后开始解析、构建render tree等一系列操作。这就导致浏览器无法并行下载所需的样式文件。
其次,多个@import会导致下载顺序紊乱。在IE中,@import会引发资源文件的下载顺序被打乱,即排列在@import后面的js文件先于@import下载,并且打乱甚至破坏@import自身的并行下载。
所以不要使用这一方法,使用link标签就行。
(1)使用@import引用外部CSS文件
导入式

<style type="text/css">
@import url("css文件路径");
</style>

(2)使用link引用外部CSS文件(推荐此方法)
链接式

<link type="text/css" rel="styleSheet" href="CSS文件路径" />
1
接下来大致说一下这两种引用外部css文件方式的区别:
link:1、属于XHTML 2、优先加载CSS文件到页面
@import1、属于CSS2.1 2、先加载HTML结构在加载CSS文件。
总结:@import相当于就是把标签放在页面的底部,所以从优化性能的角度来看,应该尽量避免使用@important命令。
2.6 优化回流与重绘
首先我们来了解一下什么是回流(reflow)和重绘(repaint),
a.重绘:
重绘是指css样式的改变,但是元素的大小和尺寸不变,而导致节点的重新绘制。
b.回流:
回流(reflow)是指元素的大小、位置发生了改变,而导致了布局的变化,从而导致了布局树的重新构建和渲染。
2.6.1 减少回流
重排会导致浏览器重新计算整个文档,重新构建渲染树,这一过程会降低浏览器的渲染速度。如下所示,有很多操作会触发回流,我们应该避免频繁触发这些操作。

改变font-size和font-family
改变元素的内外边距
通过JS改变CSS类
通过JS获取DOM元素的位置相关属性(如width/height/left等)
CSS伪类激活
滚动滚动条或者改变窗口大小
此外,某些CSS属性具有更好的回流性能。如使用Flex时,比使用inline-block和float时重排更快,所以在布局时可以优先考虑Flex。
2.6.2 避免不必要的重绘
当元素的外观(如color,background,visibility等属性)发生改变时,会触发重绘。在网站的使用过程中,重绘是无法避免的。不过,浏览器对此做了优化,它会将多次的重排、重绘操作合并为一次执行。不过我们仍需要避免不必要的重绘,如页面滚动时触发的hover事件,可以在滚动的时候禁用hover事件,这样页面在滚动时会更加流畅。
此外,我们编写的CSS中动画相关的代码越来越多,我们已经习惯于使用动画来提升用户体验。我们在编写动画时,也应当参考上述内容,减少重绘重排的触发。
最后需要注意的是,用户的设备可能并没有想象中的那么好,至少不会有我们的开发机器那么好。我们可以借助Chrome的开发者工具进行CPU降速,然后再进行相关的测试,降速方法如下图所示。

如果需要在移动端访问的,最好将速度限制更低,因为移动端的性能往往更差。
2.7 去除无用的CSS
一般情况下,会存在这两种无用的CSS代码:一种是不同元素或者其他情况下的重复代码,一种是整个页面内没有生效的CSS代码。对于前者,在编写的代码时候,我们应该尽可能地提取公共类,减少重复。对于后者,在不同开发者进行代码维护的过程中,总会产生不再使用的CSS的代码,当然一个人编写时也有可能出现这一问题。而这些无用的CSS代码不仅会增加浏览器的下载量,还会增加浏览器的解析时间,这对性能来说是很大的消耗。所以我们需要找到并去除这些无用代码。

特别申明:本文内容来源网络,版权归原作者所有,如有侵权请立即与我们联系(cy198701067573@163.com),我们将及时处理。

Tags 标签

前端csshtml5

扩展阅读

加个好友,技术交流

1628738909466805.jpg