浏览器渲染原理
- 为什么JavaScript是单线程的
- 为什么js阻塞页面加载
- css加载会造成阻塞
- DOMContentLoaded和load的区别
- 什么是CRP,即关键渲染路径?
- defer和async的区别
- 浏览器的回流和重绘
- 什么是渲染成合并
进程和线程是操作系统的基本概念。进程是cpu资源分配的最小单元,是能具有资源和独立运行的最小单位。线程是cpu调度的最小调度,是建立在进程基础的一次程序运行单位。
chrome是多进程组成的,每个进程都有自己的核心职责,相互配合完成浏览器的整体功能。每个进程之间包含多个线程,一个进程之内的多个线程也会协同工作。新开一个tab页面,新建一个进程,单个tab页面崩溃不会影响到整个浏览器。第三方插件崩溃不会影响整个浏览器。系统会为浏览器新开的进程分配内存,cpu等资源,内存和cpu资源的消耗也会变大。
浏览器 --- 浏览器进程 --- 主进程
--- 第三方插件进程
--- GPU进程
--- 渲染进程 --- js引擎线程
--- 事件触发线程
--- 定时触发器线程
--- 异步http请求线程
--- GUI渲染线程
主线程: 负责浏览器界面的显示和交互,各个页面的管理,创建和销毁其他进程。网络资源管理,下载等
第三方插件进程: 各种类型的插件对应着一个进程,仅当使用该插件时才创建。
GPU进程: 最多只有一个,用于3D绘制
渲染进程: 称为浏览器渲染进程或浏览器内核,内部是多线程的。主要负责页面渲染,脚本执行,事件处理等。
GUI渲染线程: 负责渲染浏览器界面,解析 HTML,CSS,构建 DOM 树和 RenderObject 树,布局和绘制等。当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。GUI 渲染线程与 JS 引擎线程是互斥的,当 JS 引擎执行时 GUI 线程会被挂起(相当于被冻结了),GUI 更新会被保存在一个队列中等到 JS 引擎空闲时立即被执行。
js引擎线程: javascript引擎,也成为js内核,负责处理JavaScript脚本程序。JS 引擎一直等待着任务队列中任务的到来,然后加以处理,一个 Tab 页(renderer 进程)中无论什么时候都只有一个 JS 线程在运行 JS 程序。GUI 渲染线程与 JS 引擎线程是互斥的,所以如果 JS 执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
事件触发线程: 归属于浏览器而不是 JS 引擎,用来控制事件循环。当 JS 引擎执行代码块如 setTimeOut 时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX 异步请求等),会将对应任务添加到事件线程中。当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待 JS 引擎的处理。由于 JS 的单线程关系,所以这些待处理队列中的事件都得排队等待 JS 引擎处理(当 JS 引擎空闲时才会去执行)。这些处理队列中的事件都得等js引擎处理
定时触发器线程: setInterval和setTimeout所在的线程。通过单独线程来计时并触发定时。
异步http请求线程: 在XMLHttpRequest在连接后是通过浏览器新开一个线程请求。在检测到状态变化的时候,如果有设置回调函数,异步线程就产生状态变事件,将这个回调事件队列中。在由JavaScript引擎中执行。
浏览器渲染流程![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/4af6f25526b94a42818353446ac609d3.png)
- 解析html代码,生成一个dom树
- 解析css代码,生成CSSOM树
- 将dom树和CSSOM树结合,去除不可见的元素,生成Render Tree
- 计算布局,(回流 | 重排)根据Render Tree进行布局计算,得到每一个节点的几何信息
- 绘制页面,(重绘)GPU根据布局信息绘制
细致点
-
- 解析HTML
-
- 样式计算
-
- 布局
-
- 分层
-
- 绘制: 为每一层生成如何绘制的指令
-
- 分块
-
- 光栅化
-
- 画
为什么js会阻塞页面加载
由于JavaScript是可以操纵dom的时候,如果在修改元素属性的时候,同时渲染页面,就会使得JavaScript线程和ui线程同时运行,渲染线程前后获得到元素数据就可能不一致。所以GUI渲染线程和JavaScript引擎应该是互斥关系。当 JavaScript 引擎执行时 GUI 线程会被挂起,GUI 更新会被保存在一个队列中等到引擎线程空闲时立即被执行。
css加载是否会造成阻塞
DOM和CSSOM通常是并行构建的,所以CSS加载不会阻塞DOM的解析。render tree是依赖于DOM Tree和CSSOM Tree的,必须等到CSSOM tree构建完成,也就是CSS资源加载完成,或者是css资源加载失败后,才能开始渲染。这样说来, CSS加载会阻塞DOM的阻塞。JavaScript是可以操纵DOM和css样式,如果在修改这些元素属性同时渲染界面,渲染线程前后获得的元素数据就可能不一致。为了防止出现渲染不可预期的结果,浏览器设置GUI渲染线程与JavaScript引擎为互斥关系。样式表会在后面的js执行前加载完毕,所以css会阻塞后面js的执行
DOMContentLoaded与load的区别
当domcontentloaded事件触发的时候,仅当dom解析完成后,不包括样式表,图片。若文档中没有脚本,浏览器解析完文档便能触发domcontentloaded事件。如果文档中包含脚本,则脚本会阻塞文档的解析,则脚本需要等cssom构建完成才能执行。在任何情况下,DOMContentLoaded的触发不需要等待图片等其他资源加载完成。
当onload事件触发的时候,页面上所有的DOM, 样式表,脚本,图片等资源已经加载完成
DOMContentLoaded -> load
什么是CRP,即关键渲染路径
关键渲染路劲是浏览器将html css JavaScript转换为在屏幕上呈现的像素内容所经历的一系列不走。
- 关键资源的数量: 可能阻止网页首次渲染的资源
- 关键路径长度: 获取所有关键资源所需要的往返次数和总时间
- 关键字节: 实现网页首次渲染所需要的总字节数,等同于所有关键资源传送文件大小的总和
优化DOM
- 删除不需要的代码和注释包括空格,做到最小化文件
- 使用GZIP压缩文件
- 结合HTTP缓存文件
优化CSSOM
缩小,压缩以及缓存同样重要,CSSOM会阻止页面呈现。
- 减少关键CSS元素数量
- 声明样式表的时候,密切关注媒体查询的类型。
优化JavaScript
当浏览器遇到script的标记的时候,会阻止解析器继续操作,直到cssom构建完毕,JavaScript才会运行并继续完成DOM构建过程。
- async: 当我们在script标记上添加async属性之后,浏览器遇到这个script标签的时候,会继续解析dom,同时脚本也不会被cssom阻止,不会阻止CRP
- defer: 脚本文件需要等文档解析后(DOMContrentLoaded事件前)执行。async和他的主要区别就是执行。
- 脚本不会修改DOM或者CSSOM的时候,使用async
- 预加载: preload和prefetch
- DNS预解析: dns-prefetch
分析并用关键资源数,关键字节数,关键路径长度来描述crp
最小化资源树: 消除内联,推迟下载defer,使用异步解析async
优化关键字节数(缩小,压缩)来减少下载时间
优化加载剩余关键资源的顺序: 让关键资源css尽早下载以减少crp长度
defer和async的区别
浏览器的回流和重绘
回流 重排
当renderer tree中部分或者全部元素的尺寸,结构,或者某些属性发生改变的时候,浏览器重新渲染部分或者全部文档的过程称为回流。
- 页面首次渲染
- 浏览器窗口大小发生改变
- 元素尺寸或位置发生改变导致内容变化
- 元素字体大小变化
- 添加或者删除可见的dom元素
- 激活css伪类
- 查询某些属性或调用某些方法 offsetWidth offset
- width height
- fontsize
- display position
- float
- table layout
重绘
某些元素的外观被改变所触发的浏览器行为(重新计算节点在屏幕中的绝对位置并渲染的过程)
性能影响
回流比重绘的代价更高。浏览器会维护一个队列,把所有引起回流和重绘的操作放入队列中,如果队列中的任务数量或者时间间隔达到一个阈值的,浏览器就会将队列清空,进行一次批处理,这样可以把多次回流和重绘变成一次。
css
- 避免使用table布局
- 尽可能在DOM树的最末端改变class
- 避免设置多层内联样式
- 将动画效果应用到position属性为absolute或fixed的元素上
- 避免使用css表达式
JavaScript - 避免频繁操作样式,最好一次性重写 style 属性,或者将样式列表定义为 class 并一次性更改 class 属性。
- 避免频繁操作 DOM,创建一个 documentFragment,在它上面应用所有 DOM 操作,最后再把它添加到文档中。
- 也可以先为元素设置 display: none,操作结束后再把它显示出来。因为在 display 属性为 none 的元素上进行的 DOM 操作不会引发回流和重绘。
// javascript const renderEle = document.getElementById('demo'); // 第一次操作修改 color、background、padding renderEle.style.display = 'none'; // 导致重排(重排会引起渲) renderEle.style.color = 'red'; // DOM不存在渲染树上不会引起重排、重绘 renderEle.style.background= '#ccc';// DOM不存在渲染树上不会引起重排、重绘 renderEle.style.padding = '15px 20px';// DOM不存在渲染树上不会引起重排、重绘 // ... // 第二次操作修改 marginLeft、marginTop renderEle.style.marginLeft = '15px';// DOM不存在渲染树上不会引起重排、重绘 renderEle.style.marginTop = '15px';// DOM不存在渲染树上不会引起重排、重绘 // ... // 第三次操作修改 border renderEle.style.border = '2px solid #ccc';// DOM不存在渲染树上不会引起重排、重绘 renderEle.style.display = 'block';// 导致重排(重排会引起渲)
- 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
- 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流
什么是渲染层合并
渲染层合并,对于页面中 DOM 元素的绘制(Paint)是在多个层上进行的。在每个层上完成绘制过程之后,浏览器会将绘制的位图发送给 GPU 绘制到屏幕上,将所有层按照合理的顺序合并成一个图层,然后在屏幕上呈现。