浏览器的底层渲染机制

842 阅读6分钟

1、CRP:critical rendering path 关键渲染路径

围绕渲染的机制和步骤,去详细的进行每一步的优化,以此来提高页面的渲速度和运行性能

2、从服务器基于HTTP网路请求回来的数据

 *   + 16进制的文件流
 *   + 浏览器把它解析为字符串(HTML字符串)
 *   + 按照W3C规则识别成为一个个的节点「词法解析」
 *   + 生成xxx树

3、访问页面,首先请求回来的是一个HTML文档,浏览器开始自上而下渲染

 *   + 进程:一般指一个程序(浏览器打开一个页面,就相当于开了一个进程)
 *   + 线程:进程中具体去执行事务的东西,一个线程同时只能干一件事情
 *   一个进程中,可能会包含一到多个线程

4、 同步编程:一般是只有一个线程去处理事情,上面的事情处理不完,下面的事情无法处理「一件事一件事去干」

5、异步编程:

 *    + 多线程异步编程
 *    + 单线程异步编程(JS是EventQueue+EventLoop机制完成单线程异步编程的)
 *    + ...
 *     

6、浏览器是可以开辟多个进程/线程的

 *    + GUI渲染线程:渲染页面
 *    + JS引擎线程:渲染JS代码的
 *    + HTTP网络线程,可以开辟N多个:从服务器获取资源和数据的
 *    + 定时器监听线程
 *    + DOM监听线程
 *    + ...  
 

7、渲染页面过程中

 *    + 遇到style内嵌样式,GUI直接渲染即可(同步一步一步的渲染)
 *      ->如果CSS代码量比较少,我么直接内嵌即可,拉取HTML的时候,同时CSS也回来了,渲染的时候直接就渲染了
 *      ->但是如果CSS代码比较多,如果还用内嵌,一方面会影响HTML的拉取速度,也不利于代码的维护,此时还是用外链的方式比较好
 *    + 遇到link,浏览器开辟一个HTTP线程去请求资源文件信息,同时GUI继续向下渲染「异步」
 *      + 浏览器同时能够发送的HTTP请求是有数量限制的(谷歌:5~7个)
 *      + 超过最大并发限制的HTTP请求需要排队等待
 *      ->HTTP请求一定是越少越好...
 *    + 遇到@import,浏览器也是开辟HTTP线程去请求资源,但是此时GUI也暂停了(导入式样式会阻碍GUI的渲染),当资源请求回来之后,GUI才能继续渲染「同步」
 *      ->真实项目中应该避免使用@import 
 

8、

(1) 遇到 《script src='xxx/xxx.js'》,会阻碍GUI的渲染

index.html:

   <!DOCTYPE html>
    <html>

    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
        <script src="./test.js"></script>
    </head>

    <body>
        <div id="box"></div>

    </body>

    </html>   

index.js:

var id = document.getElementById('box')
console.log(id)

输出结果:

解决方案:

  • DOM树渲染完触发

  • 页面所有的资源都加载完之后触发

(2) defer:和link是类似的机制了,不会阻碍GUI渲染,当GUI渲染完,才会把请求回来的JS去渲染...

(3)async:请求JS资源是异步的「单独开辟HTTP去请求」,此时GUI继续渲染;但是一但当JS请求回来,会立即暂停GUI的处理,接下来去渲染JS...

(4) 正常的script标签和defer以及async的渲染情况(parser是GUI渲染阶段、net是浏览器开辟的http请求阶段,execution是代表执行阶段)

(4)假如我们有5个JS的请求,如果不设置任何属性,肯定是按照顺序请求和渲染JS的「依赖关系是有效的」;但是如果设置async,谁先请求回来就先渲染谁,依赖关系是无效的;如果使用defer是可以建立依赖关系的(浏览器内部在GUI渲染完成后,等待所有设置defer的资源都请求回来,再按照编写的依赖顺序去加载渲染js);

例如我们这样的顺序去加载js,都设置了async,有可能是报错的,因为如果test先回来,就会立即执行;并不会等到GUI渲染完再执行;依赖关系是无效的

index.html:

test.js:

执行结果:

使用defer是可以的,不会出现报错的情况:浏览器内部在GUI渲染完成后,等待所有设置defer的资源都请求回来,再按照编写的依赖顺序去加载渲染js

(5)总结:真实项目开发,我们一般把link放在页面的头部「是为了在没有渲染DOM的时候,就通知HTTP去请求CSS了,这样DOM渲染完,CSS也差不多回来了,更有效的利用时间,提高页面的渲染速度」;我们一般把JS放在页面的底部,防止其阻碍GUI的渲染,如果不放在底部,我们最好设置上async/defer...;

(6)Webkit浏览器预测解析:chrome的预加载扫描器html-preload-scanner通过扫描节点中的 “src” , “link”等属性,找到外部连接资源后进行预加载,避免了资源加载的等待时间,同样实现了提前加载以及加载和执行分离。(只是提前去请求,但是机制还是按照我们分析的)

9、

(1)回流和重绘

(2)性能优化:避免DOM的回流

 *    + (GUI渲染)DOM TREE(DOMContentLoaded事件触发) -> 「中间可能执行JS」? -> (GUI渲染)CSSOM TREE -> RENDER TREE渲染树「浏览器未来是按照这个树来绘制页面的」-> Layout布局计算「回流/重排」-> Painting绘制「重绘」{ 分层绘制 }

分层绘制:
* + 页面第一次渲染,必然会引发一次回流和重绘 * + 如果我们改变了元素的位置和大小,浏览器需要重新计算元素在视口中的位置和大小信息,重新计算的过程是回流/重排,一但发生了回流操作,一定也会触发重绘「很消耗性能:DOM操作消耗性能,90%说的都是它」 * + 但是如果只是一些普通样式的改变,位置和大小不变,只需要重绘即可

如果页面出现了死循环,会一直转圈加载,不会报错;但是如果是请求的文件回不来了 ,那么是会提示错误的,栈溢出也会报错的

10、DOM树

11、CSSDOM

12、Render-Tree

13、总结步骤

总结步骤:

处理 HTML 标记,构建 DOM 树

处理 CSS 标记,构建 CSSOM 树

将 DOM 树和 CSSOM 树融合成渲染树

根据生成的渲染树,计算它们在设备视口(viewport)内的确切位置和大小,这个计算的阶段就是回流 => 布局(Layout)或 重排(reflow)

根据渲染树以及回流得到的几何信息,得到节点的绝对像素 => 绘制(painting)

优化方案:

(1):CSS 标签语义化和避免深层次嵌套(深层次嵌套之后dom树会很深)

CSS选择器渲染是从右到左(a.box a 的性能比较:a更高;因为.box a 是先找到a,然后再找.box下面的a)

尽早尽快地把CSS下载到客户端(充分利用HTTP多请求并发机制)

style

link

@import

放到顶部

(2):JS

避免阻塞的JS加载

async

defer

放到底部