👩:种一棵树最好的时间是十年前,其次是现在!
🌝:2021-07-07 又是阳光明媚的一天啊~
🙈:by 我的小小惠
首先我们来了解一下「CRP」这个概念👇
CRP:即critical rendering path缩写,中文译文:关键渲染路径,那具体的是什么呢?
在每一个渲染的环节,了解其底层运行机制,从而做相关的优化
那就不得不讲讲「线程 & 进程」啦~
线程&进程
浏览器打开一个页面就是开辟一个进程(程序),在这个页面中要干很多事情,所以需要分配多个线程去处理这些事情,一个线程同时只能干一件事。
通俗版(说人话):
假设有一个饭店,这个饭店就是一个进程,雇了很多服务员,那一个服务员就是一个线程,
很多服务员就是多线程。那就可以同时干很多事儿啦~~
- 进程大:进程中包含一到多个线程
- 浏览器是多线程的
- GUI渲染线程:自上而下渲染页面的(gpu渲染ui)
- JS引擎线程:渲染和执行JS代码的
- 时间触发线程:时间绑定的时候,会有一个线程监听事件是否触发,一旦事件触发,这个线程帮助我们通知绑定的方法执行
- 定时器触发线程:设置定时器后,分配一个线程去监听是否到达时间,当到时间后,通知对应的方法执行
- 异步HTTP请求线程:分配一个线程从服务器端获取内容「css/js...」
- WebWorker
- ...
- JS是单线程的:因为浏览器只分配了一个线程「JS引擎线程」去渲染js 当浏览器从服务器端获取到HTML页面(代码)后,会分配一个GUI渲染线程,自上而下去渲染解析代码。
从css角度出发
-
@1 如果遇到(link/img)等标签,分配一个新的线程(http请求线程)去获取资源文件,于此同时GUI会继续向下渲染,无需管资源是否回来,
“不阻碍GUI渲染” -
@2 如果遇到(style),GUI此时会继续渲染代码,把内嵌样式渲染解析了。「
但是如果在这之前,基于link发送过css资源的请求,那么此时不渲染,需要等到css资源请求回来,按照先后顺序渲染」 -
@3 如果遇到@import 'xxx.css' GUI停止渲染,分配一个新的HTTP线程去获取资源文件,必须等到资源文件回来,GUI才会继续渲染,把获取的CSS代码进行解析...
“阻碍GUI渲染”
- ===>优化技巧:
-
项目中尽可能不要使用@import 「排除less/sass中的@import ,因为这些代码编译完的时候,@import 就没有了」;
-
如果css代码较少,可以直接使用内嵌式即可,减少http请求次数,让代码渲染更及时,移动端经常这样做。页面第一次打开速度更快。但是css代码较多就使用外链式「link」,这样方便代码维护,也不至于请求html页面就要很长时间。
-
浏览器渲染页面四部曲
第一步:生成“DOM TREE”- 渲染和解析DOM结构,规划好节点和节点之间的关系
- “DOM TREE”生成后,会触发一个js事件:DOMContentLoaded ===>优化:避免过深的DOM层级嵌套
第二步:等待所有css资源请求回来后,按照导入的顺序,依次渲染css样式,要保证css的渲染顺序和优先级等问题 -->生成一个“CSSOM TREE”
===>优化:我们把link放在HEAD标签中,在渲染DOM之前就发送资源请求,这样等待DOM TREE生成之前,CSS已经请求回来,此时直接渲染即可,让事情同时去做,可以提高页面第一次渲染的速度。
页面同时并发的HTTP请求数量是
5-7个「限定同源」,而且过多请求会导致网络通道阻塞...等很多原因导致,多次发送http请求,不如只发送一次请求快,所以真是项目中,我们需要把所有的css资源合并为一个文件!!这样无需考虑多个资源的渲染顺序
-
第三步:把生成的"DOM TREE"和"CSSOM TREE"合并在一起,共同创建为“RENDER TREE”(渲染树)- 渲染数中,包含了最后浏览器渲染的时候,每个节点应该具备的样式「包含:自己写的样式、继承父级样式、浏览器默认的样式」。 window.getComputedStyle("元素")
-
第四步:浏览器开始按照RENDER TREE进行渲染- @1 Layout布局/排列:根据浏览器当前视口(viewport)大小,计算出每个节点在视口中的位置
- @2 分层(文档流):脱离文档流,构建每一层文档流,并且规划好每一层如何绘制「绘制的步骤都计算好」
- @3 Painting绘制:按照分析好的规则开始绘制页面,最后在浏览器的视口中呈现出我们的页面
遇到script资源请求时,会发生什么呢?
-
默认情况下遇到script,都会阻碍GUI渲染
- @1 发送HTTP请求,获取资源文件,此时GUI渲染停止
- @2 资源获取到之后,交给JS引擎线程去渲染和解析JS
- @3 JS解析完成,GUI继续
- 如果给script设置 defer或者async 则不会阻碍GUI渲染
-
script:请求资源和执行JS都会阻碍GUI
-
script async:请求资源不会阻碍GUI「开辟新的HTTP线程去请求,GUI渲染继续」,当资源请求回来后,会立即渲染解析JS,此时中断GUI渲染,当JS执行完,GUI才会继续
-
script defer:请求资源不会阻碍GUI,而且不会管资源啥时候获取到,都要等到GUI渲染完,并且所有的JS资源(设置defer的)都请求回来,最后按照导入顺序依次执行js「和link特别像」
-
async和defer区别:async不会考虑JS依赖关系,谁先请求回来谁先执行,但是defer需要等待所有资源都回来,GUI也渲染完了,此时再去按照依赖的顺序去执行JS
script是同步,如果加了defer和async是异步
提取前端性能优化方案
-
@1 避免HTML层级结构嵌套太深,
目的:加快DOM TREE的生成 -
@2 CSS选择器渲染顺序从右到左,所以CSS选择器避免前缀过长,
目的:加快CSSOM TREE生成(a{} VS .box a{}) -
@3 优先使用style内嵌样式
目的:减少HTTP请求次数,加快CSS渲染 -
@4 样式过多的情况下使用,但是要把CSS资源合并为一个CSS样式文件「webpack可以自动打包」
目的:减少HTTP请求并且link不会阻碍GUI的渲染 -
@5 把link放在页面头部,
目的:创建DOM TREE的同时,去请求资源文件 -
@6 坚决不用@import 「排除sass/less」,因为@import 会阻碍GUI渲染
-
@7 我们一般把script放在页面的末尾,如果非要放在顶部,最好设置defer或者async
目的:防止其阻碍GUI的渲染 -
@8 真实项目中,也需要把所有的JS资源合并为一个JS文件,
目的:减少HTTP请求=====>所有的优化目的,都是让页面渲染出来的速度更快,白屏等待的时间更短
重排(回流)
- 页面初次渲染,必然会经历一次Layout排列(重排),计算出每个节点在视口中的位置
- 当”删除或新增DOM元素、改变DOM元素位置、元素尺寸发生变化、内容发生变化...“等行为出现的时候,浏览器需要重新计算每个几点在视口中的位置(重新计算布局信息),也就是Layout重新来一遍,这样的操作非常消耗页面渲染性能,这就是重排(回流)。
重绘
-
页面初次渲染必然会经历一次重绘,也就是Painting绘制,绘制出页面
-
当”修改了元素的某些样式,例如:文字颜色、背景颜色、背景图片等“,这些样式不会影响页面的布局结构,此时我们只需要Painting的操作重新来一遍,这就是”重绘“。
重排必然会引发重绘,因为它必须经历:Layout->分层->Painting这个完整阶段 -
我们平时所说的操作DOM耗性能,大部分指的就是DOM重排所以减少DOM的重排,是前端性能优化一个重要的指标
-
浏览器的渲染队列机制
- 在当前上下文中,如果遇到修改DOM样式操作,不会立即去修改,而是先放在”渲染队列“中,然后看后面是否还存在修改样式的操作,如果有则继续放在队列中...等到当前上下文执行完毕,会把队列中所有修改样式的操作一次性执行处理,这样只会引发一次DOM重排
- 如果遇到获取元素样式的代码,则直接”刷新渲染队列“「意思是:把现在队列中修改样式的操作,立即进行处理」
如何减少DOM重排呢?
- 1、放弃直接操作DOM,使用VUE/REACT等框架,基于数据驱动,实现视图渲染「本质:框架本身把DOM操作进行了封装,在内部实现了对DOM的优化处理」
- 2、读写分离:把修改样式和获取样式的代码分离开”集中修改或集中获取“
- 3、批量新增或修改元素:文档碎片或者模板字符串拼接
- 4、使用CSS3硬件加速(GPU加速)
- 修改元素的”transform/opacity...“等样式,不会引发原始文档流中的DOM重排,修改元素的transform,会把元素单独脱离出一个文档流,后期浏览器只是重新计算这个文档流中的位置和布局,对原始的其他文档流不会有任何影响
- 同样的道理,我们后期修改元素样式,尽可能修改那些脱离文档流的元素样式,这样后期重新计算布局信息的时候,也只是对这层文档流重新计算,总比全部重新计算好很多
//这样会引发10次重排
for (let i = 0; i < 10; i++) {
let span = document.createElement("span");
span.innerHTML = i;
document.body.appendChild(span);
}
//这样会引发1次重排「基于innerHTML设置字符串内容,很容易吧容器中之前的内容/事件干掉」
let str = ``;
for (let i = 0; i < 10; i++) {
str += `<span>${i}</span>`;
}
document.body.innerHTML = str;
//基于文档碎片实现DOM的批量设置:文档碎片就是DOM节点的临时容器
let frag = document.createDocumentFragment();
for (let i = 0; i < 10; i++) {
let span = document.createElement("span");
span.innerHTML = i;
frag.appendChild(span);
}
document.body.appendChild(frag);
`小惠不易,随手点赞哦😝