浏览器的渲染机制
浏览器的渲染机制:1解析HTML,2样式计算,3布局,4分层,5绘制,6分块,7光栅化,8画。经过这样的一个流程,HTML字符串->像素信息,最终呈现在标签页上。
值得注意的是,浏览器的复杂程度极高,可以视为一个成熟的操作系统,在解释浏览器的渲染机制时,我们将从底层的进程以及线程的通信和写作讲解。比如,浏览器的网络线程"Network Service",在Chrome浏览器使用"shift+esc"快捷键可以看到,如下图。
当浏览器向服务器获取某个标签页的HTML资源,网络线程会将“渲染任务”加入到消息队列,当渲染主线程空闲,会按顺序从消息队列拿取渲染任务,开始渲染。
一,解析HTML。这一步,我们将得到DOM和CSSOM两种树。但是第一步不是这么容易,我们知道html文件往往还有CSS和JS链接,此时浏览器有不同的处理过程。
对于CSS代码,解析过程可能导致界面白屏,但是并不影响DOM的构建。原理是,1DOM和CSSOM是并行构建的,2不同的link标签,按照顺序进行串行下载解析,最终结合一个CSSOM树,然后DOM+CSSOM会进行后续步骤,如果在这一步卡顿,就会导致“第一次渲染”卡顿,出现白屏。在Chrome,F12->Performance,可以看到“First Contentful Paint”在CSS加载之后。环境与媒体查询不匹配,也会影响CSS解析导致白屏。注意:如果后续的link下载解析失败,会使用已经解析好了的CSSOM。浏览器为了提高解析效率,会启动一个预解析器先下载和解析CSS代码。
对于JS,浏览器会停止一切行为,等待JS下载执行完之后才可以继续(此时,预解析线程会分担下载JS的任务,然后渲染主线程执行JS)。这样做的原因是,JS可能会影响界面样式,影响CSSOM,避免影响后续的操作,JS的任务必须优先执行。
二,样式计算。拿到第一步的DOM和CSSOM,浏览器执行"样式计算",将DOM的节点根据CSSOM进行样式添加,原本普通节点->Computed Style节点
三,布局。接着对DOM树进行布局操作,得到Layout树。这时候添加的是一些位置信息,这一点很重要,影响到后面知识点“重绘和重排”。需要注意的是,DOM树和Layout树不一定是对应的,有以下情形,1display:none,在Layout树中的该节点就会消失。2节点::before,伪元素会在Layout树上添加上。3自动添加匿名行盒和匿名块盒。
四,分层。根据位置信息,可以把不同子树放在不同层上。显示触发分层will-change:transform属性。隐式触发有视频元素、Canvas等。可以在浏览器的调试区域直观的看到,F12->右上角三个点->找到图层(layers),就可以像3D建模一样查看标签页上不同元素处于哪个图层,一共有几个图层等。需要注意,分层和后续的分块一样,都是为了加快并优化后续的界面变化,分出不同层,可以快速定位重叠元素,互不影响各自的重绘任务。不过,分太多的层(小于50),也会造成占用内存过高等负面影响。分层的策略是,将静态的页面归为一层,动态区域单独分层。
五,绘制。承上启下,为每一层生成如何绘制的指令,同时也是渲染主进程的最后一个工作,接下来进入到其他线程完成任务。这里就是浏览器渲染的进化,传统方式全部交给渲染主线程,会导致线程压力过大,而阻塞页面渲染。
六,分块。每一层都要分成更小的区域。这一步是由多个线程同时进行。渲染主线程将任务交给合成线程,合成线程去线程池喊来一帮“小弟”,这些线程作为分块器,分别执行不同区域的分块任务,完成后合成线程执行下一步,光栅化
七,光栅化。将每一块变成位图,这里会先处理靠近视口的块。并且会调用GPU进程来加速
八,画。合成线程完成了光栅化,得到了位图,就可以“画”。此时合成线程计算出每个位图在屏幕上的位置,交给GPU进行最终呈现。
总结:渲染过程相当复杂,浏览器调用了多个进程、线程来完成,包括网络线程、预解析线程、渲染主线程、合成线程、线程池的空闲线程和GPU进程,不同进程线程协调合作,最终将画面呈现。
扩展问题,1重绘和重排,2transform效率为什么高,3CSSOM为什么一定要串行构建?1重排,获取或者更改节点的位置信息会触发重排,而更改颜色等样式就会触发重绘。重排的开销更大,因为影响到了Layout树,就是第三步的布局,要重新推倒,再次计算布局树。所以重绘不一定会触发重排,重排一定是重绘,因为其余样式的改变对于布局树的影响不大,不一定会触发重新计算布局树。2transform属性是启动了合成层属性,属性变化时,跳过布局和重绘,直接进入“画”。应用transform的元素会被提升为独立合成层,拥有专属的GPU纹理缓存。修改transform时,合成线程重新计算图层位置,同志GPU进程更新,操作显存中的纹理缓存,执行图层合成。3因为CSS存在优先级的问题,每一个标签解析完都要进行优先级比较,重新定义CSSOM的状态。
浏览器的缓存机制
浏览器的缓存机制分为“强制缓存”和“协商缓存”。
首先,需要明确的是,浏览器在向服务器发送请求获取资源之前都会向浏览器缓存先进行一步请求,类似于后端向数据库请求数据之前,可以先向内存redis求取。如果请求结果存在且资源未失效,则可以直接返回,实现强制缓存;反之,两个条件任何一个不满足,则协商缓存。
其次,“强制缓存”。实现区域是在http请求头中,机制分为cache-control和expires。由于后者存在浏览器和服务器所在地区不同,导致的时差问题,缓存是否有效变的难以界定。在http/1.1就已经被淘汰,现在主要使用cache-control。
接着是,“协商缓存”。正如之前介绍的,当请求浏览器缓存并未找到或者已经失效,则浏览器将再向服务器发送请求。实现区域也是请求头,实现机制也是两种。现在主要使用ETAG/IF-NOT-MATCH。所谓EATG,就是资源的一种用哈希实现的“id”,资源在缓存和服务器中的ETAG应该保持一致,这种“id”可以被视为一种指纹,每个人的指纹都不一样,即使是双胞胎,那么一个资源即使发生了轻微的变化,也是两个不同的资源,拥有两个指纹。当第一次向缓存请求资源的时候,就已经拿到了etag。这个etag的值,将会在第二次向服务器请求的时候,在请求头中,设置为if-not-match。服务器拿到之后会和这个值作比较,如果有变化则说明缓存的信息非最新的,于是设置statusCode=200,并且返回该资源;如果两个值一模一样,则说明缓存的信息就已经是资源最新状态,可以使用,于是statusCode=304,重定向到浏览器缓存,将缓存重新设置为有效,并且返回这个资源。
最后,这是我用ai做的我认识中的思维导图,将原理和流程解释清楚,如图。
浏览器的跨域处理
跨域是现代“前后端分离”架构的常见问题,处理手段也很常见,主要包括“前端使用代理”、“后端同意”和古老的“JSONP”。
首先,需要注意的是,跨域问题,只存在浏览器中,小程序开发、服务端开发是遇不到的。浏览器作为客户端和服务器的中间代理人,是要核对双方是否“认识”,不认识的话,就会被视为不安全访问,受到限制。其次,同源-->协议+主机+端口,有一处不一致就是不同源。
接下来,先介绍“前端代理”。原理很简单,直接访问另一个服务器(同源服务器),然后该服务器把请求转发给目标服务器,这样不经过浏览器就完美避开了浏览器的跨域限制
然后,就是“后端认证”。更广泛的叫法是CORS,操作就是在服务器的响应头中添加认证,告诉浏览器,我允许xxx访问,于是就可以通信了。有两种请求的区别,“简单请求”和“预检请求”。简单请求的定义是:1POST/GET方法,2安全请求头(标准请求头,不要新增,不要修改),3请求体的格式text/plain,text/json,text/form-data。面对简单请求,服务器只需要在响应头中添加Access-Control-Allow-Origin: <允许的域名>,经过浏览器的“同源”检验之后放行。面对非简单请求,也就是预检请求,浏览器会先向服务端发送OPTIONS请求,验证服务器是否允许,预检请求通过之后发送实际请求。Access-Control-Request-Method+Access-Conyrol-Request-Headers,这些是预检请求的响应头设置。