上篇文章已经介绍了,在URL加载时数据请求过程发生的一些数据的交互以及涉及面试中可能存在的面试点。本篇继续介绍“输入URL到页面加载的全过程”中数据渲染部分的内容。本篇文章也可以说是对于浏览器渲染流程的一个简易化介绍。
当数据请求返回服务端的相应结果(html文件)后,浏览器会根据返回的Html文件进行页面的渲染,从拿到数据到最终显示到页面上,浏览器也是做了大量的工作的,大致原理可参考下图。
- 浏览器解析HTML,构建DOM数
- 解析CSS,构建Style Tree规则树
- 通过DOM Tree 和 style Rules构建 Render Tree
- 通过Render Tree进行界面节点计算布局
- 绘制界面
大致的流程就像上面的图示和列出的渲染流程一样,接下来我们通过下面的几个问题从侧面来了解网页的渲染过程。
线程:线程是进程的一个实体,是进程的一条执行路径。线程是CPU独立运行和独立调度的基本单位。
线程和进程的区别主要体现在如下几个方面:
-
进程的地址空间和资源分配相互独立,而同一进程的各线程间共享。 -
进程间通信需要通过IPC(无名管道、有名管道、信号机制、信号灯、共享内存、消息队列、套接字socket),线程间可以通过全局变量,信号量,互斥锁就可以实现通信。 -
线程上下文切换比进程上下文切换快得多。所以较为频繁切换上下文的可以使用线程实现。 -
一个进程内有多个线程
2、Chrome访问一个网页启动了多少个进程?
-
浏览器进程:负责控制浏览器除标签页外的界面,包括地址栏、书签、前进后退按钮等,以及负责与其他进程的协调工作,同时提供存储功能。 -
GPU进程:负责整个浏览器界面的渲染。Chrome刚开始发布的时候是没有GPU进程的,而使用GPU的初衷是为了实现3D CSS效果,只是后面网页、Chrome的UI界面都用GPU来绘制,这使GPU成为浏览器普遍的需求,最后Chrome在多进程架构上也引入了GPU进程。 -
网络进程:负责发起和接受网络请求,以前是作为模块运行在浏览器进程里面的,后面才独立出来,成为一个单独的进程。 -
插件进程:主要是负责插件的运行,因为插件可能崩溃,所以需要通过插件进程来隔离,以保证插件崩溃也不会对浏览器和页面造成影响。 -
渲染进程:负责控制显示tab标签页内的所有内容,核心任务是将HTML、CSS、JS转为用户可以与之交互的网页,排版引擎 Blink 和 JS 引擎 V8 都是运行在该进程中,默认情况下Chrome会为每个Tab标签页创建一个渲染进程。
如上,这是浏览器中我们常见的进程。其中浏览器进程,GPU进程,网络进程是共享的,同一个站点共用一个渲染进程。其中最后网页的渲染就是发送给GPU实现页面的渲染的。
3、渲染进程中包含哪些线程?
- GUI渲染线程:GUI渲染线程负责渲染浏览器界面HTML元素,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。在Javascript引擎运行脚本期间,GUI渲染线程都是处于挂起状态的,也就是说被冻结了。
- JavaScript 引擎线程:这就是我们常说的js单线程,主要是为了实现JS文件的解析和运行,包括执行用户的交互操作、dom树修改、css样式更新等操作。需要注意的是若js执行过程会导致GUI渲染线程阻塞,它们不可同步执行。
- 定时触发器线程:浏览器定时计数器(setTimeout、setInterval )并不是由JS引擎计数的, 因为JS引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确, 因此通过单独线程来计时并触发定时是更为合理的方案。
- 异步Http请求线程:在XMLHttpRequest在连接后是通过浏览器新开一个线程请求,将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到JS引擎的处理队列中等待处理。
- 事件处理线程:事件处理线程管理着任务队列,异步任务的回调函数会放在其中,等 执行栈中的任务执行完毕后,读取其中任务执行。如鼠标点击、滑动、异步http请求等的回调处理。
4、如何构建渲染树?
当我们生成 Dom Tree 和 CSSOM(style Rules又称样式规则),就需要将它两合并起来。合并过程需要注意几点:
- render Tree 只包含可见的节点,如”display:none;”则不会在render Tree中显示
- 从解析构建到生成Render Tree这个过程:构建DOM、构建CSS rules、构建Render Tree、布局。这几个过程并不是严格按照顺序执行的。渲染引擎会以最快的速度展示内容,所以第二阶段不会等到第一阶段结束才开始,而是在第一阶段有输出的时候就开始执行。其它阶段也是如此。由于浏览器会尝试尽快展示内容,所以内容有时会在样式还没有加载的时候展示出来。 这就是经常发生的FOCU(flash of unstyled content)或白屏问题。
5、如何理解回流与重绘?
如下情况发生回流:
- 页面首次加载
- 浏览器尺寸变化
- Dom结构改变,添加或者删除dom元素或者内部文本发生改变
- 改变节点元素的几何信息:margin、padding、width、height、display:none、border、fontSize
- 激活CSS伪类,如hover、focus等
- 获取某些属性的值,浏览器会为了获得准确的值也会触发回流的过程
clientWidth、clientHeight、clientTop、clientLeft
offsetWidth、offsetHeight、offsetTop、offsetLeft
scrollWidth、scrollHeight、scrollTop、scrollLeft
scrollIntoView()、scrollIntoViewIfNeeded()
getComputedStyle()
getBoundingClientRect()
scrollTo()
IE的 currentStyle
注意:当页面中元素样式的改变并不影响它在文档流中的位置时,如color、background-color、visibility等,浏览器会将新样式赋予给元素并重新绘制它,这个过程重绘而不回流。
如何减少回流和重绘:
- 使得节点脱离文档流
- 使用 transform 替代 top
- 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)
- 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局
- 不要把节点的属性值放在一个循环里当成循环里的变量。
- 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame
- CSS 选择符从右往左匹配查找,避免节点层级过多
- 将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点。比如对于 video 标签来说,浏览器会自动将该节点变为图层。
- 批量修改样式
- 减少对于某些属性的值(offsetWidth等),获取后可对其进行存储,避免多次频繁获取使用
6、async和defer是什么?为什么需要使用到它们?
上图展示了三种不同script加载声明下,html与script解析加载顺序情况。其中绿线代表HTML解析、蓝线代表js下载、红线代表js执行。首先来介绍下dom解析、css解析、js解析运行在html加载中的执行顺序问题。
- 后端返回html文档
- html开始进行dom解析,从html标签开始进行解析
- 浏览器发现link标签,发出CSS文件的请求,服务器返回这个CSS文件
- 浏览器发现script标签(无 async 和defer),向服务器发起请求返回js文件,并运行js代码(这时候DOM停止解析),js包含对css的操作,这时候html暂停dom解析,开始下载所有的css文件,然后解析cssom,
- 执行完js文件后继续dom解析
- 合并dom和cssom生成render tree
- 布局绘制界面
结合图示发现,常规的script标签不仅仅会影响dom的解析也会影响cssom的解析【cssom原本和dom解析应是同步进行的】,那么如何防止js打乱html的解析过程呢,如下:
- 将js放置在body底部
- 使用defer或aysnc减少js对html解析的感染,defer目前应该是最贴近我们的要求的
总结上面的可以发现:defer模式文档的解析不会停止,其他线程将下载脚本,待到文档解析完成,脚本才会执行。async模式文档的解析不会停止,其他线程将下载脚本,脚本下载完成后开始执行脚本,脚本执行的过程中文档将停止解析,直到脚本执行完毕。
如下有几个关于defer和aysnc的特点需要了解:
- defer和aysnc只适合外联脚本
- 多个defer脚本,浏览器会根据顺序下载和执行,但是多个async,它的下载和执行都是异步无序的。
- defer脚本会在DOMContentLoaded和load事件之前执行
- async会在load事件之前执行,但并不能确保与DOMContentLoaded的执行先后顺序
本篇文章首发于微信公众号“胖蔡话前端”,如果觉得文章对你有用的话, 欢迎微信搜一搜“胖蔡话前端”,或扫码右侧微信公众号二维码支持一下作者。