碎碎念
正式开始学习Javascript,从T16.x开始慢慢更新Javascript中的学习笔记与感悟
本篇文章包含以下几个内容:
- 白屏与FOUC
- 浏览器渲染机制
- reflow&repaint
- 同步&异步、async&defer
前置知识
在探讨白屏问题和FOUC无样式内容闪烁问题出现的原因之前,我们要知道的一个原则是浏览器在对于图片和CSS, 在加载时会并发加载(如一个域名下同时加载两个文件). 但在加载 JavaScript 时,会禁用并发,并且阻止其他内容的下载。
白屏与FOUC并不是bug,它仅仅是不同浏览器渲染机制的差异,及不同的代码顺序写法,下面来解析下两种现象的成因。
白屏
现象:浏览器页面在加载时,出现段时间的白屏、无内容现象;
原因:在chrome浏览器的加载和渲染机制中,需要等待所有CSS都被加载、解析完成后,再进行页面渲染。如果CSS样式放在底部,浏览器在渲染完dom后,发现CSS还没有加载、解析,因此只好再去加载CSS。这个加载等待的时间就出现了长时间的白屏。
FOUC
现象:无样式内容闪烁,表现为先展示了没有样式(浏览器默认样式)的网页内容,再突然展示加载了样式的网页内容,中间的用户体验形象的称之为『闪烁』。
当我们输入网址后,浏览器会向服务端发起请求,然后服务端将页面发送给浏览器,浏览器边下载页面边进行解析,过程如下:
- 边下载HTML文档边构建DOM树
- 浏览器会先以浏览器默认样式来解析CSSOM Tree
- DOM Tree + CSSOM Tree构建出了渲染树,然后页面内容渲染出来
- 当解析到内联、内部样式时,马上刷新CSSOM Tree,引起Render Tree的变化
- 当解析到外部样式时,会先加载,然后解析和刷新CSSOM Tree
上述五个步骤由于样式文件在下载过程中的延时受网络环境与电脑性能的影响,导致我们看到两种截然不同的样式的闪烁变化
第二种出现原因是,CSS样式表被放置在页面底部,浏览器会先加载没有样式的HTML页面,等CSS加载完,再渲染一次,这就是FOUC。
CSS、JS文档推荐的引入顺序
- CSS放在
<head>
中,保证优先加载样式
原因:在body渲染前,先构建一个相对完整的CSSOM Tree
- JS放在最接近
body
闭合标签处,防止JS的加载阻塞后面标签与样式的加载
浏览器渲染机制
webkit为内核的浏览器工作流程:
下面我们具体看下当浏览器渲染一个页面时,发生了什么?
- 浏览器从服务器获取到HTML文档,并构建了DOM(文档对象模型)
- 样式被载入及解析,构建为CSSOM(层叠样式表模型)
- 在完成了DOM和CSSOM的基础上,渲染树将会被创建,代表了一系列将被渲染的对象(在webkit内核的浏览器中被称之为renderer或者render object,在Gecko中,其被称之为frame)。渲染树映射了除了不可见的元素,例如
<head>
和display:none
之外的所有DOM结构,每一个文本字符串都划分在不同的渲染对象中,每一个渲染对象都包含了它相应的DOM对象以及计算后的样式。换句话说,渲染树是DOM的直观表示。
- 渲染树的每一个元素包含的内容都是计算过的,我们称之为『布局』。浏览器使用一种流式处理的方法,只需要一次操作就可以对所有的元素进行布局(表格则需要多次)。
- 最后,布局完成,渲染树将转化成屏幕上的实际内容,我们称之为『painting』。
两个重要的概念
Repaint/重绘
当页面元素样式的修改不影响其在文档流中的位置(几何位置,元素坐标),仅影响元素的外观时(background-color,visibility),浏览器只会将新样式赋予元素,并进行重绘。
触发条件:
不涉及DOM元素或排版上的变化,如元素的color
,text-align
,另外伪类引起的颜色变化也不会造成reflow,仅仅触发repaint。
Reflow/重排
当样式改变影响到文档内容或结构时,浏览器就触发Reflow操作(可以理解为重新布局),重新计算页面元素位置或几何结构。
触发条件:
- DOM操作,即对元素的增删改,调整顺序等
- 内容变化,包括文本的改变
- CSS属性的更改,或者是重新计算
- 增删样式表内容
- 浏览器窗口变化
- 伪类样式(:hover等)造成的元素表现得改动
对比
从性能角度考虑,Reflow的成本要比Repaint高很多。Dom Tree里面每个节点都有Reflow方法,一个节点的reflow会导致子节点,父节点或者兄弟节点reflow,进而导致性能较低的电脑,或是移动端设备体验很差。
因此,在页面制作过程中,应该尽可能的考虑浏览器性能问题,减少Reflow能够减少浏览器引擎的重新渲染,提高页面加载效率。
哪些样式是高消耗的?
具体哪些属性是高消耗的呢?就是绘制前需要浏览器进行大量运算的样式属性。
- box-shadows
- border-radius
- transparency(图像透明度)
- transforms
- CSS filters(性能杀手)
async和defer
前置知识:同步与异步
日常理解中的同步与异步的语义与计算机中的同步、异步是大相径庭的。前者在日常对话中往往会说“这几件工作我们同步进行”,异步就是将事情分开进行。
在计算机语言中,同步可以理解为,将事情一件一件,按顺序往下进行,而异步理解为几件事情同时进行,语义与现实生活中的理解正好相反。
defer与async属性
<script src="script.js"></script>
如上,在无defer与async情况下,浏览器会立即加载并执行该脚本。立即指的是,在渲染该文件 后面的其它元素之前,也就是会阻塞后面HTML元素的解析。只有等脚本解析与执行完后,浏览器才会继续解析其它的元素。
defer与async是脚本异步加载的两种方式,只对外部脚本有效。我们来具体看看
<script src="script.js" defer></script>
加入了defer,实现了脚本文件与HTML文档的异步加载,也就是脚本文件的加载与加载后续文档元素是并行的。这里要注意:脚本文件的执行会相应的defer到所有元素解析完成后。
《Javascript高级程序设计》中2.1.2提到,应用了defer属性的脚本会先于DOMContentLoaded时间执行。但是,在实际情况中延迟脚本不一定按照顺序执行,也不一定在DOMContentLoaded事件触发前执行。因此,使用defer最大的弊端是执行顺序是未知的,同样也发生在async上。
<script src="script.js" aysnc></script>
有 async,加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行(异步)。
总结:
- defer和async在脚本加载方式上都是异步执行的;
- 差别在于脚本加载完成后,执行脚本的时机;
- 从执行顺序上看,defer是按照加载顺序执行脚本的,而async是乱序执行的,因为只有某个脚本文件加载完成他就立即执行,因此顺序无法保证;
- 所以,从使用场景上看,defer是最接近我们用途的,因为他完全不考虑各个脚本之间的依赖,但它可以用在不受依赖,独立的脚本上;