浏览器渲染过程
首先要从服务器获取需要渲染的内容,此时获取到的是文件流(进制编码内容),那么接下来浏览器会把16进制字节信息编译为代码字符串,然后按照W3C规则进行字符解析生成对应的tokens,再转换成浏览器内核可以识别渲染的DOM节点,最后再按照节点解析为对应的DOM树和CSSOM树。
(1)在CSS资源还没有请求回来之前,先生成DOM树(DOM的层级关系以及节点关系);
=> 该过程中如果遇到JS先把JS执行
正常情况下JS会阻碍GUI渲染,所以JS一般放在页面尾部,就是为了确保DOM树生成完才会加载JS;也可能会与defer和async异步管控JS请求;
(2)当所有CSS请求回来之后,浏览器按照CSS导入顺序依次进行渲染,最后生成CSSOM树;
(3)把DOM树和CSSOM树结合在一起,生成有样式有结构的RENDER TREE 渲染树;
(4)浏览器按照渲染树在页面中进行渲染和解析;
- 布局(Layout)或回流/重排(reflow)—— 计算元素在设备视口中的大小和位置;
- 绘制/重绘(painting)—— 根据渲染树以及回流得到的几何信息,得到节点的绝对像素;
link、@import引入的区别
link和@import都是导入外部样式(服务器获取样式文件),区别如下:
//link
<link href="css/style.css" rel="stylesheet" type="text/css"></link>
//import
@import ...;
-
link——GUI渲染不会停止,浏览器会派发新的线程(HTTP线程)去加载资源文件,无论CSS是否请求回来,代码继续渲染;
-
@import——GUI渲染暂时停止渲染,去服务器加载资源文件,请求没有返回之前是不会继续渲染的;
-
如果是嵌入式style,GUI直接渲染;
script标签中defer和async有什么区别
script会阻碍GUI渲染,先请求js(http线程),在把请求回来的执行,只有js执行完再去GUI渲染;
defer:GUI继续渲染,同时http去请求,请求回来也不会立即执行,而是等到GUI渲染完再去按照之前引入的script顺序依次执行。
async:GUI继续渲染,同时http去请求,当请求回来后立即先执行js,GUI暂停,js执行完之后GUI继续。谁先回来谁执行(适用于没有依赖顺序的)。
用图来说明一下更加容易理解:
link不会阻碍GUI渲染,GUI继续向下,link会开启一个新的http线程去加载css代码;
性能优化(基于渲染机制优化)
1、减少DOM树渲染时间(HTML层级不要太深,标签语义化);
2、减少CSSOM树渲染时间(选择器从右向左解析,尽可能减少选择器层级);
3、减少HTTP请求次数和请求大小;
4、一般会把CSS放在页面的开始位置(提前请求资源,用link不用import,对于移动断如果css较少尽可能使用嵌入式);
5、避免白屏,可快速生成loading渲染树(前端骨架屏);
6、服务器的SSR骨架屏所提高的渲染是避免了客户端再次单独请求数据,而不是样式和结构上的首屏处理;
7、把JS放在页面底部以及尽可能使用defer或者async;
8、CRP性能节点优化;
预处理机制
预先把所有请求资源在GUI渲染之前就去发送请求,http并发限制(6-7);
回流(重排)和重绘
回流/重排——元素位置/大小/页面布局和几何信息变化,触发重新布局,导致渲染树重新计算布局和渲染,例:元素位置(移动)、尺寸(width/height/padding/margin/border)、内容发生变化等,页面一开始渲染时(无法避免,因为回流根据视口大小来计算元素位置和大小,浏览器窗口尺寸变化也会引发回流);
重绘——元素样式改变(宽高、大小、位置不变),eg:outline、color、background-color、visibility等;
回流比重绘更耗费性能。
注:回流一定会触发重绘,重绘不一定会回流。
box.onclick=function(){
//这两行修改操作只引发一次回流
box.style.width = "200px";
box.style.height = "200px";
}
box.onclick=function(){
//引发两次回流,因为一旦遇到获取样式的代码需要把之前队列中的样式进行渲染
box.style.width = "200px";
console.log(box.offsetWidth);
box.style.height = "200px";
}
浏览器渲染队列机制:遇到修改样式代码,浏览器没有立即渲染,而是先把它放到渲染队列中,继续看下面是否还是修改样式的,是的话继续放进去......直到遇到获取元素样式的代码或者没有修改样式的代码了,则立即把队列中的样式统一进行渲染,最后只引发一次回流重绘。
避免回流重绘:
-
分离读写(获取和修改分开);
box.style.left = "200px"; box.style.top = "200px"; box.offsetLeft; box.offsetTop; -
缓存布局;
div.style.left = div.offsetLeft - 1 + 'px'; => //先获取值再使用 var curLeft = div.offsetLeft; div.style.left = curLeft - 1 + 'px'; -
元素批量修改;
let frg = document.createDocumentFragment(); for(let i=0;i<10;i++){ let span = document.cereateElement('span'); span.innerHTML = i; frg.appendChild(i); } document.body.appendChild(frg); //创建文档碎片,引发一次回流