上一篇文章解释了浏览器的渲染过程,简单说了一下什么是CRP,这篇文章来记录一下如何进行CRP优化
浏览器渲染(解析)页面的过程如下:
- 导航:输入URL、DNS解析、建立连接
- 响应:发送请求、接收第一次响应的HTML文本
- 解析:构建DOM、构建CSSOM、编译javascript
- 渲染:样式(Style,构建Render树)、布局计算(Layout,回流/重排)、绘制(Paint,重绘)。
其中解析和渲染步骤比较重要
我们将解析和渲染的步骤中关键的五步提取出来,作为浏览器的关键渲染路径,优化关键渲染路径可提高渲染性能
这五步分别是
- 第一步是处理HTML标记,构建DOM树
- 第二步是处理CSS,构建CSSOM树。
- 第三步是将DOM和CSSOM组合成一个Render树。
- 第四步是在依据渲染树计算每个节点的大小和位置。
- 最后一步是根据渲染树和回流得到的几何信息,得到节点的绝对像素,将各个节点绘制到屏幕上。
即:构建DOM->构建CSSOM->构建Render树->回流->重绘。
详细参考上一篇文章 浏览器渲染过程
CRP优化之-解析阶段优化
构建DOM树时遇到样式
构建DOM树时遇到样式,有以下几种情况
-
遇到style内嵌样式,GUI直接渲染
- 所以项目中如果CSS代码量比较少,直接内嵌即可,拉去HTML的时候,同时CSS也回来了,渲染的时候直接就渲染了
- 但是如果CSS代码比较多,如果还用内嵌,一方面会影响HTML的拉取速度,也不利于代码的维护,此时还是用外链的方式比较好
-
遇到link标签(异步),浏览器开辟一个HTTP线程去请求资源文件信息,同时GUI继续向下渲染
tips:浏览器同时能够发送的HTTP请求是有数量限制的(谷歌:5~7个),超过最大并发限制的HTTP请求需要排队等待,所以HTTP请求一定是越少越好。
-
遇到@import(同步),浏览器也是开辟HTTP线程去请求资源,但是此时GUI也暂定了(导入式样式会阻碍GUI的渲染),当资源请求回来之后,GUI才能继续渲染
所以真实项目中应该避免使用@import
构建DOM树时遇到js
遇到 <script src='xxx/xxx.js'>
,有三种解析的方式,分别是 默认,async,defer,默认情况下会阻碍GUI的渲染
如图所示三种方式的渲染规则:
(图片中, 'net'
表示请求js文件 , 'execution'
表示执行(解析或者叫渲染)js)
如果不想阻碍GUI渲染,该怎么办?在标签里加属性 async
defer
- defer:和link是类似的机制了,不会阻碍GUI渲染,当GUI渲染完,才会把请求回来的JS去渲染。
- async:请求JS资源是异步的(单独开辟HTTP去请求),此时GUI继续渲染;但是一但当JS请求回来,会立即暂停GUI的处理,接下来去渲染JS
假如我们有多个JS的请求,如果不设置任何属性,肯定是按照顺序请求和执行JS的(依赖关系是有效的);但是如果设置async,谁先请求回来就先渲染谁,依赖关系是无效的;如果使用defer是可以建立依赖关系的。浏览器内部在GUI渲染完成后,等待所有设置defer的资源都请求回来,再按照编写的依赖顺序去加载渲染js。只有写在浏览器的开头或者中间才需要这样写,写在浏览器结尾和默认方式一样
tips:遇到img,音视频,也是和link的机制一样,异步加载,继续渲染GUI。
总结
所以真实项目中开发是:
- link标签放在头部:一般把link放在页面的头部(是为了在没有渲染DOM的时候,就通知HTTP去请求CSS了,这样DOM渲染玩,CSS也差不多回来了,更有效的利用时间,提高页面的渲染速度)
- script标签放在底部:我们一般把JS放在页面的底部,防止其阻碍GUI的渲染,如果不放在底部,我们最好设置上async/defer;
解析阶段优化方案
根据以上原理的具体优化方案:
-
标签语义化和避免深层次嵌套。加快构建DOM树的过程。
-
css选择器不要过长。CSS选择器渲染是从右到左的,加快构建CSSOM的过程。
-
尽早尽快地把CSS下载到客户端(充分利用HTTP多请求并发机制),将style、link、@import放到顶部
-
避免阻塞的JS加载使用async、defer属性,或者将script标签放到底部
CRP优化之-渲染阶段优化
渲染阶段主要优化回流和重绘
虽然现在项目都用vue和react来写,DOM操作已经被极大的简化,很少考虑DOM操作的性能问题,但是当我们自己封装不依赖框架的功能性组件插件的时候,性能仍然是不可忽视的问题
DOM的重绘和回流Repaint & Reflow
回流:元素的大小或者位置发生了变化(当页面布局和几何信息发生变化的时候) , 触发了重新布局,导致渲染树重新计算布局。 如
- 添加或删除可见的DOM元素;
- 元素的位置或尺寸发生变化;
- 内容发生变化(比如文本变化或图片被另一个不同尺寸的图片所替代);
- 页面一开始渲染的时候(这个无法避免);
- 因为回流是根据视口的大小来计算元素的位置和大小的,所以浏览器的窗口尺寸变化也会引发回流
重绘:元素样式的改变(但宽高、大小、位置等不变)。如 outline
, visibility
, color
, background-color
等
注意:回流-定会触发重绘。而重绘不一定会回流
避免DOM的回流重绘
-
放弃传统操作dom的时代,基于vue/react使用数据影响视图模式。
mwm/ mvc / virtual dom/ dom diff
-
css集中改变
举例:
<head> <style> #box { ackground: lightcoral; width: 500px; height: 50px; } </style> </head> <body> <div id="box"></div> <script src="test.js"></script> </body>
test.js
let box = document.querySelector('#box'); box.style.width = '100px'; box.style.height = '100px';
如果直接这样写,不会出现样式改变的过程,因为在DOM解析的时候,最后会执行js,执行完js再构建DOM树,这时候box的样式已经变为100px,仅仅会执行一次回流和重绘,就是最开始的那一次。 所以我们这样写:
test.js
setTimeout(()=>{ let box = document.querySelector('#box'); box.style.width = '100px'; box.style.height = '100px'; },1000)
以上操作在老版本的浏览器中会改变两次回流重绘,因为改变了两次样式
这样写将央视集中改变可以减少次数
box.style.cssText = "width:100px;height:100px;";
而新版浏览器不会重绘两次,新版浏览器有一个渲染队列机制。接下来再详细说明
-
分离css读写操作( 现代的浏览器都有渲染队列的机制)
连续改变两次样式,在新版浏览器不会重绘两次。新版浏览器有一个渲染队列机制,会把所有要改变的样式依次放在渲染队列里面,然后把渲染队列中的样式渲染一次。
当修改样式的代码已经没有了或者遇到了获取元素样式的代码,都会刷新渲染队列:即把现有队列中的样式去统一修改和渲染一次,引发一次回流和重绘
获取样式的方式:
style.xxx
getComputedStyle
getBoundingClientRect
offsetTop
、offsetLeft
、offsetWidth
、offsetHeight
、clientTop
、clientLeft
、clientWidth
、clientHeight
、scrollTop
、scrollLeft
、scrollWidth
、scrollHeight
、getComputedStyle
、currentStyle
...
以下会引起两次回流和重绘
如果有一个需求,让其在原始宽度的基础上加100
box.style.width = parseFloat(window.getComputedStyle(box)['width']) + 100 + 'px'; box.style.height = parseFloat(window.getComputedStyle(box)['height']) + 100 + 'px';
改为:
let prevW = parseFloat(window.getComputedStyle(box)['width']), prevH = parseFloat(window.getComputedStyle(box)['height']);0 box.style.width = prevW + 100 + 'px'; box.style.height = prevH + 100 + 'px';
-
元素批量修改
使用文档碎片减少回流: createDocumentFragment
以下会触发十次回流与重绘
let box = document.querySelector('#box'), for (let i = 0; i < 10; i++) { let span = document.createElement('span'); span.innerHTML = i + 1; box.appendChild(span); }
改为
let box = document.querySelector('#box'), frag = document.createDocumentFragment();//文档碎片 for (let i = 0; i < 10; i++) { let span = document.createElement('span'); span.innerHTML = i + 1; frag.appendChild(span); } box.appendChild(frag)
或者用模板字符串拼接方式:
let box = document.querySelector('#box'), str = ` ` ; for (let i = 0; i < 10; i++) { str += ` <span>${i+1}</span> ` ; } box.innerHTML = str;
-
动画效果应用到position属性为absolute或fixed的元素上(脱离文档流) 因为在重回的时候,是分层冲毁的,每一个文档流单独进行重绘,所以将动画效果单独放到一个文档流上面,可以减少性能开销
-
CSS3硬件加速( GPU加速) 比起考虑如何减少回流重绘,我们更期望的是,根本不要回流重绘;
transfom
\opacity
\filters
这些属性会触发硬件加速,不会引发回流和重绘 可能会引发的坑:过多使用会占用大量内存,性能消耗严重、有时候会导致字体模糊等 -
回牺牲平滑度换取速度 每次1像素移动一一个动画,但是如果此动画使用了100%的CPU,动画就会看上去是跳动的,因为浏览器正在与更新回流做斗争。每次移动3像素可能看起来平滑度低了,但它不会导致CPU在较慢的机器中抖动
-
避免table布局和使用css的javascript表达式
利用渲染队列机制做一个轮播图
写一个轮播图,原理是将第一张图复制一个放在最后,然后再运动到最后的时候,无过渡跳转到第一张图,再继续运行轮播图。代码如下:
<style>
* {
margin: 0;
padding: 0;
}
.container {
position: relative;
margin: 20px auto;
width: 800px;
height: 300px;
overflow: hidden;
}
.container .wrapper {
position: absolute;
top: 0;
left: 0;
z-index: 10;
display: flex;
justify-content: flex-start;
align-items: center;
width: 4000px;
height: 100%;
/* 动画 */
transition: left .3s linear 0s;
}
.container .wrapper .slide {
width: 800px;
height: 100%;
}
.container .wrapper .slide img {
display: block;
width: 100%;
height: 100%;
}
</style>
<div class="container">
<div class="wrapper">
<div class="slide">
<img src="images/1.jpg" >
</div>
<div class="slide">
<img src="images/2.jpg" >
</div>
<div class="slide">
<img src="images/3.jpg" >
</div>
<div class="slide">
<img src="images/4.jpg" >
</div>
<!-- 克隆 -->
<div class="slide">
<img src="images/1.jpg" >
</div>
</div>
</div>
let container = document.querySelector('.container'),
wrapper = container.querySelector('.wrapper'),
step = 0,
timer;
timer = setInterval(function () {
step++;
if (step >= 5) {
// 立即回到第一张
wrapper.style.transition = 'left 0s';
wrapper.style.left = `0px` ;
// 运动到第二张
step = 1;
}
wrapper.style.transition = 'left .3s';
wrapper.style.left = `-${step*800}px` ;
}, 2000);
但是这样运行后会有问题,发现运行到最后的时候,突然跳到第二涨,并且还存在过渡。
原因是与渲染队列机制有关系,当运行step运行到5,浏览器会把四句样式修改放到渲染队列中,最后执行一次回流重绘,这样的话就只有最后两句css起作用了,就从最后一张直接拉到第二张了,而且还有过渡动画。
所以我需要让让其立即刷新一下渲染队列,先跳到第一章,再从第一张过渡到第二张。
let container = document.querySelector('.container'),
wrapper = container.querySelector('.wrapper'),
step = 0,
timer;
timer = setInterval(function () {
step++;
if (step >= 5) {
// 立即回到第一张
wrapper.style.transition = 'left 0s';
wrapper.style.left = `0px` ;
// 运动到第二张
step = 1;
// 刷新渲染队列
wrapper.offsetLeft;
}
wrapper.style.transition = 'left .3s';
wrapper.style.left = `-${step*800}px`;
}, 2000);
手动刷新渲染队列
如果不利用这个机制,那么只能这样写
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!