我们今天来了解一下浏览器工作原理,它是由呈现引擎(也就是各位道友常说的内核)来完成的。我们今天主要借助 webkit
来看一下。
渲染流程
呈现引擎一开始会从网络层获取请求文档的内容,然后开始进行下面的这个流程:
我们可以看出如下几个点:
浏览器解析HTML/CSS:
解析 HTML ,生成 DOM Tree。
解析 CSS ,生成 Style Rules(样式规则)。
Javascript 的解析主要是通过 JavaScript 解释器来进行的,不是呈现引擎的事。它主要是通过 DOM API 和 CSSOM API 来分别操作 DOM Tree 和 Style Rules。解析完成后,浏览器引擎会通过 DOM Tree 和 Style Rules 来构造 Rendering Tree。
当文档被解析并且添加 DOM 节点时,在 DOM 节点上调用称为 attach 的方法来创建渲染器。
Rendering Tree 渲染树并不等同于 DOM Tree,因为 attach 方法会计算样式信息,一些像
head
或display:none
的元素就没必要放在渲染树中了。WebKit 支持的每一个 CSS 属性都可以通过 RenderStyles (所有的样式信息都存储在上面)查询。如果 DOM 节点创建了一个渲染器,那么它使用渲染器上的 setStyle 方法将该样式信息连接到该渲染器。而火狐是通过生成 规则树 和 样式上下文树 ,来进行与 DOM Tree 的匹配。最后生成 Render Tree。
计算每个
frame
(元素) 的位置,进行 layout 。呈现引擎遍历呈现树,由用户界面后端层将每个节点绘制出来。绘制的顺序是:
- 背景颜色
- 背景图片
- 边框
- 子代
- 轮廓
总结一下工作原理:HTML/CSS parse —> Render Tree —> layout —> paint 。
需要着重指出的是,这是一个渐进的过程。为达到更好的用户体验,呈现引擎会力求尽快将内容显示在屏幕上。它不必等到整个 HTML 文档解析完毕,就会开始构建呈现树和设置布局。在解析外联脚本的同时,呈现引擎会将已解析的页面内容显示出来。
DOM 阻塞
JS 阻塞文档解析
当遇到 <script>
标签时,会立即解析并执行脚本。如果脚本是外部的,则会立即停止解析,直到脚本下载下来之后继续解析脚本。这样做是有原因的,因为 javascript 可能会有诸如 document.write 的操作,这样的话,后续所有资源的加载都是无意义的。
JS 有 defer 属性, es5还新增了一个 async 属性。他们都可以实现异步加载,不会阻塞文档解析。他们的区别就是 async 下载完了之后会立即执行,而 defer 是在遇到 </html>
才会执行(先于 DOMContentLoaded 事件)。
还有一个同样可以实现异步加载,通过 DOM 动态添加。它的执行顺序排在 defer 之后,DOMContentLoaded 事件之前。
根据上面的特性我们可以发掘出的优化js的方法有:
- 合理使用 defer 和 async 或者 DOM 动态添加
- 合并 js 文件,或者适当的内嵌,减少请求–>减少请求时间–>减少阻塞时间
- Minify Javascript
CSS 阻塞 JS
如果 js 去请求 css 的信息,这个时候 css 还没加载完毕,就会导致取到的信息错误。为了防止这一点,css 会阻塞 js 的解析。谷歌做了优化,只有脚本去访问的样式会受到尚未加载解析的 css 影响时,才会禁止此脚本。
我们可以通过 media query
来进行优化。比如:
<link href=”something.css” rel=”stylesheet” media=”(min-width: 750px)”
这样如果小于 750px
,则不会加载该 css 。
预解析
在执行脚本时,其他线程会解析剩余文档,找出并提前下载好其他资源,但是不执行。这不会更新 DOM 树,这项工作会留给主解析器来完成。
因为浏览器是有并行下载限制的,所以我们能够尽可能的减少图片等的体积,还是需要尽量减小。
总结
- 发送 HTTP 请求,获取 HTML
- 解析 HTML ,构建 DOM树
- 发送 HTTP 请求,获取 CSS 和 JS 和 IMG
- 解析 CSS 创建 RenderStyles,解析执行 JS
- 基于 viewport 生成布局
- 绘制页面
- 重复 2 - 6