探索浏览器对于HTML的渲染原理(过程)
原文链接:https://github.com/FIGHTING-TOP/FE-knowlodge-base
探索目的
为了更好地优化我们前端页面的性能,特对基础原理进行考究
大致过程
从浏览器获取到HTML文件开始,浏览器会经历解析、渲染、交互三大阶段;
解析
浏览器会在收到HTML文件的第一次响应包后开始解析(即使该HTML大于14kb),解析过程包括DOM树和CSSOM树的构建、资源的预加载(通过预加载扫描器异步加载)、JavaScript 编译以及构建辅助功能树。DOM包含了页面的所有内容,CSSOM包含了页面的所有样式。
渲染
渲染过程包括Style、Layout、Paint以及还可能会有Compositing这些阶段,
渲染器会在DOM树和CSSOM树构建好之后,将两棵树组合成一个render树,这个过程会计算所有可以显示标签的样式,可以显示的标签包括从body
开始(包括body
)没有display: none
的所有节点,包含带有visibility: hidden
的节点。
布局是浏览器从根节点开始遍历整棵render树,计算每个节点的尺寸和位置;第一次确定节点的大小和位置称为布局。随后对节点大小和位置的重新计算称为回流。如果布局完成后有图片加载完成并且该图片没有指定大小,这样就会造成回流。
绘制是将布局阶段生成的render树的多有节点转换成屏幕上的实际像素,包括文本、颜色、边框、阴影和替换的元素(如按钮和图像)。
在这个过程中,浏览器会将布局树中的元素分解为多个层,将内容提升到GPU上的层(脱离CPU上的主线程),从而提高绘制和重新绘制的性能。每一个带有一些特定的CSS属性的元素和一些特定标签元素都可以实例化一个层,像和元素,以及任何带有opacity``,3D转换
,will-change
CSS属性的元素都会和它们的子节点单独绘制一个层,当然,如果子节点满足以上条件则会再单独实例一个层。
浏览器针对处理CSS动画和不会很好地触发重排(因此也导致重新绘制)的动画属性进行了优化。为了提高性能,可以将被动画化的节点从主线程移到GPU上。将导致合成的属性包括 3D transforms
(transform: translateZ(), rotate3d()
,etc.),animating transform
和 opacity
, position: fixed
,will-change
,和 filter
。一些元素,例如 <video>
, <canvas>
和 <iframe>
,也位于各自的图层上。 将元素提升为图层(也称为合成)时,动画转换属性将在GPU中完成,从而改善性能,尤其是在移动设备上。
交互
当我们看到页面显示出来后,整个页面的所有渲染工作可能并没有完成,因为这时页面可能还无法进行点击,滚动,触摸等操作,因为这个时候可能还有js没有执行完,也就是主线程仍在占用状态,特别是像绑定了window.onLoad
这种的js逻辑。
在测试页面性能的时候,有一项重要的指标就是TTI(Time to Interactive)是从第一个请求导致DNS查找和SSL连接到页面可交互时所用的时间。
webkit 渲染流程图
gecko 渲染流程图
问题探究
CSS的加载会阻塞页面的渲染吗?
提出假设
我们来分析一下,从上面的页面渲染流程来看,HTML的渲染过程是将解析阶段生成的DOM树和CSSOM树组合成一个render树,那么CSS的加载肯定是会阻塞CSSOM树的建立,那没有CSSOM树也就没办法合成render树,因此也就没办法渲染,所以CSS的加载是会阻塞页面的渲染的。
验证假设
我们来写段代码测试一下
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script>
console.log('走js这里了')
</script>
<link rel="stylesheet" href="https://www.google.com/123123.css">
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
从结果来看,当css文件请求没有结束之前页面是空白的,等css加载失败后页面才显示内容,这就说明我们的假设成立。
在这个结果中,也可以看出css没有加载结束之前,js被执行了,那我们将js代码写在link标签的后面,他还会被执行吗?
css加载会阻塞j后面的s运行吗?
直接测试
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="https://www.google.com/123123.css">
<script>
console.log('走js这里了')
</script>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
执行结果
结果来看js是在css加载后执行了,则说明css的加载阻塞了后面js的运行。
那么如果不是内嵌的js,使用src来引入一个js文件呢?
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="./test000.js"></script>
<script>
console.log('走link标签前面的内嵌js了')
</script>
<link rel="stylesheet" href="https://www.google.com/123123.css">
<script src="./test111.js"></script>
<script>
console.log('走link后面的内嵌js了')
</script>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
在test000.js中这样写
console.log('This is test000.js')
在test111.js中这样写
console.log('This is test111.js')
那如果使用了defer或者async属性呢?
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="./test000.js" defer></script>
<script>
console.log('走link标签前面的内嵌js了')
</script>
<link rel="stylesheet" href="https://www.google.com/123123.css">
<script src="./test111.js" defer></script>
<script>
console.log('走link后面的内嵌js了')
</script>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
为什么会这样?
因为defer和async都可以使浏览器异步加载并解析执行js文件,所以link标签不能阻塞js的文件的执行,你可能会说,那为什么defer被阻塞了呢?我们知道defer和async有点不一样的,在没有内嵌js时,defer修饰的js会按照DOM先后顺序依次执行,async则是先加载完成的先执行;在有内嵌js时,无论是defer还是async都会等待内嵌js执行完才会去执行它们,如果没有这两个属性就会按照DOM中的顺序依次执行各个js。
在实际中,我们会使用一些方法来应对css加载阻塞js执行的问题,比如把js放在link之前然后使用
DOMContentLoaded
方法,或者之前在jQuery中常用的ready方法。