乐趣区

关于前端: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 class=”jac”>

  <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” class=”text”>Jartto’s blog</p>
</div>
<style>
#box{color: red;}
.text{color: yellow;}
</style>

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

<div id=”box”>
<p class=”text”>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 代码不仅会减少浏览器的下载量,还会减少浏览器的解析工夫,这对性能来说是很大的耗费。所以咱们须要找到并去除这些无用代码。

退出移动版