# 浏览器渲染原理
# 浏览器打开网页经历了什么
仅考虑最简单的情况吧
DNS解析,浏览器向DNS服务器请求解析获取IP地址
和服务器建立TCP连接, 涉及到三次握手什么的
发送HTTP请求, 返回HTTP响应,假设下载到html文件资源
Chromium 的内核有渲染引擎 Blink(基于 Webkit)和 JavaScript 引擎 V8。Chromium 多进程多线程架构
4、页面解析, 看下面
# 页面解析基础流程
以Chrome为例,浏览器拥有多个进程,其中包括渲染进程、插件进程、网络进程、主进程、GPU进程等。
我们写的HTML、CSS、JS等静态资源最后都要通过渲染进程转化为可视化界面。
页面的解析工作是在 Renderer 进程中进行的,Renderer 进程通过在主线程中持有的 Blink 实例边接收边解析 HTML 内容,每次从网络缓冲区中读取 8KB 以内的数据。浏览器自上而下逐行解析 HTML 内容,经过词法分析、语法分析,构建 DOM 树。当遇到外部 CSS 链接时,主线程调用网络请求模块异步获取资源,不阻塞而继续构建 DOM 树。当 CSS 下载完毕后,主线程在合适的时机解析 CSS 内容,经过词法分析、语法分析,构建 CSSOM 树。浏览器结合 DOM 树和 CSSOM 树构建 Render 树,并计算布局属性,每个 Node 的几何属性和在坐标系中的位置,最后进行绘制展示在屏幕上。当遇到外部 JS 链接时,主线程调用网络请求模块异步获取资源,由于 JS 可能会修改 DOM 树和 CSSOM 树而造成回流和重绘,此时 DOM 树的构建是处于阻塞状态的。但主线程并不会挂起,浏览器会使用一个轻量级的扫描器去发现后续需要下载的外部资源,提前发起网络请求,而脚本内部的资源不会识别,比如 document.write
。当 JS 下载完毕后,浏览器调用 V8 引擎在 Script Streamer 线程中解析、编译 JS 内容,并在主线程中执行
静态资源的处理, 大概可以分为下面几个步骤
DOM树构建,文档对象模型
渲染引擎使用HTML解析器(调用XML解析器)解析HTML文档,将各个HTML元素逐个转化成DOM节点,从而生成DOM树
CSSOM树构建,层叠式样式表对象模型
CSS解析器解析CSS,并将其转化为CSS对象,将这些CSS对象组装起来,构建CSSOM树
将DOM树和CSSOM树结合在一起生成渲染树
页面布局
渲染树构建完毕之后,元素的位置关系以及需要应用的样式就确定了,这时浏览器会计算出所有元素的大小和位置;
绘制
页面布局完成之后,浏览器会将根据处理出来的结果,把每一个页面图层转换为像素,并对所有的媒体文件进行解码绘制。
渲染流程 最后就是渲染出页面, 然后关闭需要关闭的TCP连接等等
# 页面性能
衡量一个页面性能的方式有很多,但能给用户带来直接感受的是页面何时渲染完成、何时可交互、何时加载完成。其中,有两个非常重要的生命周期事件,DOMContentLoaded 事件表示 DOM 树构建完毕,可以安全地访问 DOM 树所有 Node 节点、绑定事件等等;load 事件表示所有资源都加载完毕,图片、背景、内容都已经完成渲染,页面处于可交互状态。但是迄今为止浏览器并不能像 Android 和 iOS app 一样完全掌控应用的状态,在前后台切换的时候,重新分配资源,合理地利用内存
document.addEventListener('DOMContentLoaded',function(){
console.log('DOMContentLoaded');
});
Painting(绘制)的具体工作是由浏览器UI后端部分负责完成的,在Painting阶段,会调用引擎的paint api(canvas会调用draw api)进行像素级信息计算与绘制,像素级信息具体表现为帧信息(图层),浏览器会将各层的信息发送给GPU(GPU进程:最多一个,用于3D绘制等),GPU会将各层合成(composite),显示在屏幕上。
Composite 渲染层合并
页面中 DOM 元素的绘制是在多个层上进行的。在每个层上完成绘制过程之后,浏览器会将所有层按照合理的顺序合并成一个图层,然后显示在屏幕上。对于有位置重叠的元素的页面,这个过程尤其重要,因为一旦图层的合并顺序出错,将会导致元素显示异常。 GPU加速:让需要进行复杂动画的元素(或所在元素)单独拥有一个合成图层。 1、合成层的位图,会交由 GPU 合成,比 CPU 处理要快, 2、当需要 repaint 时,只需要 repaint 本身,不会影响到其他的层 3、对于 transform 和 opacity 效果,不会触发 layout 和 paint
然后我们再看下如何变成合成层,如何应用GPU加速(硬件加速):
- 3D 或透视变换(perspective transform) CSS 属性
- 使用加速视频解码的
<video>
元素 拥有 3D - (WebGL) 上下文或加速的 2D 上下文的
<canvas>
元素 - 混合插件(如 Flash)
- 对自己的 opacity 做 CSS动画或使用一个动画变换的元素
- 拥有加速 CSS 过滤器的元素
- 元素有一个包含复合层的后代节点(换句话说,就是一个元素拥有一个子元素,该子元素在自己的层里)
- 元素有一个z-index较低且包含一个复合层的兄弟元素(换句话说就是该元素在复合层上面渲染)
提升合成层的最好方式是使用 CSS 的 will-change属性。will-change 可以设置为opacity、transform、top、left、bottom、right。
注意事项:提升到合成层后合成层的位图会交GPU处理,但请注意,仅仅只是合成的处理(把绘图上下文的位图输出进行组合)需要用到GPU,生成合成层的位图处理(绘图上下文的工作)是需要CPU。
当需要repaint的时候可以只repaint本身,不影响其他层,但是paint之前还有style, layout,那就意味着即使合成层只是repaint了自己,但style和layout本身就很占用时间。
仅仅是transform和opacity不会引发layout 和paint,那么其他的属性不确定。
最后,也说说缺点或者说容易踩坑的地方(要学会权衡、学会克制):
合成层占用内存的问题。
层爆炸,由于某些原因可能导致产生大量不在预期内的合成层,虽然有浏览器的层压缩机制,但是也有很多无法进行压缩的情况,这就可能出现层爆炸的现象(简单理解就是,很多不需要提升为合成层的元素因为某些不当操作成为了合成层)。解决层爆炸的问题,最佳方案是打破 overlap 的条件,也就是说让其他元素不要和合成层元素重叠。简单直接的方式:使用3D硬件加速提升动画性能时,最好给元素增加一个z-index属性,人为干扰合成的排序,可以有效减少创建不必要的合成层,提升渲染性能,移动端优化效果尤为明显。
# 案例分析
浏览器打开下面的网页时,会如何处理页面渲染和资源加载
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="/a.css" />
<script src="/a.js"></script>
</head>
<body>
<div>a</div>
<script src="/b.js"></script>
<div>b</div>
</body>
</html>
- 浏览器会先解析head标签内的dom, 得到a.css和a.js两个资源的引用。同步下载完毕后解析执行。
- 浏览器解析body标签里面的dom,得到b.js的引用, 此时浏览器会立即请求b.js的资源,加载完毕后执行脚本。然后继续解析剩余的dom。所以如果b.js资源比较大,或者网路比较差时,页面有可能会先显示a,知道资源加载完毕并且执行完后再显示b。
# 面试解答
# 1、为什么网页中经常把css文件放在head里面,而js文件放在body尾部?
因为head中的资源会在浏览器解析body之前加载完毕并且执行,样式需要在body解析前生效,否则用户有可能看到一闪而过样式崩坏的画面。js的逻辑一般在DomContentLoaded的事件回调中执行,所以很多情况下, js并不需要放在head头部。因为在浏览器加载head引用的资源时, 页面会处于完全空白的状态,而放在body尾部,可以让页面更快的呈现出来,带来更好的用户体验。
# 2、script标签的async和defer属性
两者都会并行下载js, 并且不会影响页面的解析。
defer被设定用来通知浏览器该脚本将在文档完成解析后,触发 DOMContentLoaded
事件前执行。
有 defer
属性的脚本会阻止 DOMContentLoaded
事件,直到脚本被加载并且解析完成。
async 则是下载完立即执行。(两者执行顺序不确定,执行阶段不确定,可能在 DOMContentLoaded
事件前或者后 )
1、两者都不会阻止 document 的解析 2、defer 会在 DOMContentLoaded 前依次顺序执行 (可以利用这两点哦!) 3、async 则是下载完立即执行,不一定是在 DOMContentLoaded 前 4、async 因为顺序无关,所以很适合像 Google Analytics 这样的无依赖脚本
当初始的 HTML 文档被完全加载和解析完成之后,DOMContentLoaded
事件被触发,而无需等待样式表、图像和子框架的完全加载。
window.onload
页面上所有的资源(图片,音频,视频等)被加载以后才会触发load事件,简单来说,页面的load事件会在DOMContentLoaded被触发之后才触发