我们了解的一些优化点包括(后面我们会反推为何要这么做 以便来理解浏览器的渲染过程):
减少http请求书(可以使用雪碧图啊 文件合并啊 dataurl啊)
把js文件最好放在页面底部
css选择器的嵌套层级最好不要超过三级
css选择器里尽可能少使用tag标签
display:none与visible:hidden的关系和区别
缓存dom节点
将dom的修改样式和获取样式分开来写
而在了解这些浏览器渲染过程后,我们会了解
DOMContentLoaded, window.onload, document.ready
正文:
浏览器渲染基本步骤(简述):构建dom树,构建css树,构建渲染树,根据渲染树计算节点空间属性,绘制到屏幕上
浏览器渲染基本步骤(复杂点的):
1:从服务器拿到html文件后 开始构建dom树
2:如果遇到style节点 则构建css树(这里不会阻塞dom树的构建)
3:如果遇到链接则另辟线程去下载文件
4:如果遇到script标签,则立即执行script标签(会阻塞dom树的构建)
5:在浏览器从构建dom树和css树的过程中,渲染树也马不停蹄的在构建,
6:根据渲染树计算节点空间属性
7:绘制节点
问题:
1:dom树 css树 渲染树是并行执行的嘛
2:渲染树的节点和dom树的节点一致吗
3:在页面初始化过程中 重排与重绘是不断的重复执行吗 如果是页面应该会出现闪烁,如果不是 重排是否应该在所以的文件全部下载完成后才开始?
回答:
1:应该是并行执行的,浏览器会以尽可能快的速度渲染界面,渲染书没有必要等到dom树全部构建好和css树全部构建好才开始构建,而是只要dom树和css树输出一点 渲染书就构建一点
2:渲染树里面只包含可见元素 head里面的元素都不可见,所以不会有,display:none的元素不可见 也不会有,而visibie:hidden在css中规定其虽隐藏却占据着位置,所以会被包含
3:重排与重绘不会等待所有文件下载好后才开始(我们经常会看到网页中已经有内容,而浏览器的loading仍在滚动),而重排是在渲染书构建好之后才开始还是和渲染书一起开始,我目前不知道,如果是你写一个浏览器的话 你是会等待渲染器全部构建好之后再去进行重排还是一边构建渲染器一边进行重排呢
根据优化点来解读浏览器渲染过程:
1:减少http请求数
浏览器在渲染页面的时候首先构建dom树,在构建dom树的过程中发现链接就去下载,是想如果一个界面中有30个小图片 10个css文件 8个js文件那就是48个待下载文件,当前浏览器能够支持的最高并发http请求应该是8个,那一般的都是4个,所以浏览器如果想要全部加载完需要48/4=12次http请求(理想情况下),如果我们把每次http请求比做桥梁 ,每次请求(过桥)都需要消耗固定的时间和性能,那我们肯定是希望过桥后一次性把所有东西取出来
2:把js文件放在底部加载
浏览器在构建dom树的时候,在解析html节点时遇到script标签会阻塞当前解析进程,而改为立即执行script标签里面的代码,所以如果我们希望用户尽可能快的看到界面 我们肯定是不希望解析过程被阻塞,所以我们把script标签放在最底部,这样等所有dom节点全部被解析后再去执行script标签里面的代码,
3:css选择器的嵌套层级最好不要超过3级
在构建dom树的同时,遇到css属性,浏览器会同时开始构建css树,浏览器对选择器的解析规则是从右到左解析的,比如.box .left p
,会在页面中找到所有的p标签
,然后在p标签
中找其父元素有.left类
的p元素
,再找祖父元素有.box
的p标签
。在这里 如果可能的话 直接省去中间的.left是否有可能呢,这里只是个比喻,过多的不必要的嵌套层级会增加解析的成本
4:css选择器里尽可能少使用tag标签
假如界面中有50个span标签,然后我们有一个css是这样写的:.main span{css样式},那么因为浏览器是从右到左解析的,所以浏览器会先查找出所有的span标签,在去找其中祖先元素class中有main的标签,如果我们给收盘加一个calss为sub,.main .sub:这样浏览器只需要查找所有class中存在sub的标签 然后查找其祖先元素里class包含main的元素
5:display:none与visible:hidden的关系和区别
在dom树和css树构建的过程中,渲染书也在不断的构建,渲染书里面只会存在将要显示的元素,所以如果有一个元素我们一开始不需要显示他,需要某些条件触发才会去显示,我们当然不希望这个有可能不显示的元素 被放入渲染树来增加渲染成本,那你会用display:none还是visibility呢,用visibility的话该元素会被放入渲染书,用display:none的话不会被放入渲染树,但是当显示的时候因为visibility已经存在了 所以他不会触发重排,而display:none,本来没有占据空间 所以他会触发重排
6:缓存dom节点
每次通过document.getElementById获取元素的时候 浏览器都会去查找元素,而层层嵌套的dom查找是非常消耗性能的,如果能把该dom节点缓存起来,而不用每次去重新查找无异是一个优化性能不错的方法
7:将dom的修改样式和获取样式分开来写
我们在说浏览器渲染过程时候:1.构建dom树,2.构建css树,3.构建渲染树,4.根据渲染树计算节点空间属性,5.绘制到屏幕上,其中4和5会在后面用户的操作过程中不断的被触发,我们把步骤4叫做重排,把步骤5当作重绘,当用户增删改dom节点或者改变dom节点css属性时候,有可能改变了dom节点的空间属性,这个时候浏览器有必要重新组织一遍渲染树,并计算该dom节点和被影响的节点的空间属性,然后进行重绘把界面更新,但是如果用户每改一下就触发重排完全是没有必要的,重排的消耗太大,重绘的性能消耗也非常大,所以浏览器会把用户的操作累积起来,然后固定频率进行重排和重绘,这个频率就是帧,我们写动画会经常与他打交道,因为重排和重绘的高昂代价 浏览器设置了一个渲染队列,用户对元素的外观修改并不会立即生效 而是会把该操作放置到渲染队列里面,当浏览器到指定刷新时间 进行重排和重绘时候,会把渲染队列里面的的操作全部执行,所以这里面是浏览器主动发起重排操作 来执行渲染队列的哦,如果你不小心主动触发了渲染队列的执行 那就会强制浏览器进行重绘,如果你不小心连续的强制浏览器执行渲染队列,那么浏览器苦心打造的固定时间间隔执行重排机制就会被你成功的打破了,而你将要面对的就是性能的损耗 界面的卡顿。。。
8:关于重排和重绘的时间损耗
var times = 15000;// code1 每次过桥+重排+重绘console.time(1);for(var i = 0; i < times; i++) { document.getElementById('myDiv1').innerHTML += 'a';}console.timeEnd(1);// code2 只过桥console.time(2);var str = '';for(var i = 0; i < times; i++) { var tmp = document.getElementById('myDiv2').innerHTML; str += 'a';}document.getElementById('myDiv2').innerHTML = str;console.timeEnd(2);// code3 console.time(3);var _str = '';for(var i = 0; i < times; i++) { _str += 'a';}document.getElementById('myDiv3').innerHTML = _str;console.timeEnd(3);// 1: 2874.619ms// 2: 11.154ms// 3: 1.282ms
关于需要了解的
window.onload会在页面所有资源加载完毕才会去执行,而jquery的document.ready则是在dom加载完毕就直接执行,而DOMContentLoaded也是在dom加载完毕就直接执行,在document.ready的底层就是通过DOMContentLoaded实现的
自己实现document.ready
如果浏览器支持DOMContentLoaded那我们完全可以通过监听DOMContentLoaded来实现document.ready,如果浏览器不支持呢,我们可以使用document.readyState来实现,IE firefox和safari全部支持document.readyState
function ready(fn){ if(document.addEventListener) { document.addEventListener('DOMContentLoaded', function() { //注销事件, 避免反复触发 document.removeEventListener('DOMContentLoaded',arguments.callee, false); fn(); //执行函数 }, false); }else if(document.attachEvent) { //IE document.attachEvent('onreadystatechange', function() { if(document.readyState == 'complete' || document.readyState == 'interactive') { document.detachEvent('onreadystatechange', arguments.callee); fn(); //函数执行 } }); } };