浏览器底层渲染机制:当我们从服务器获取代码后,浏览器是如何把代码,渲染为页面及相关效果的 CRP(关键渲染路径)性能优化法则:了解浏览器底层处理的具体步骤,针对每一步骤进行优化 Js中的同步和异步编程
- 同步编程:上一件事情没有处理完,下一步事情无法处理
- 异步编程:上一件事情即便没有处理完,也无需等待,可以继续处理后面的事情
进程和线程:一个进程中可能包含多个线程
- 进程:一般代表一个程序(或者浏览器打开一个页面就开辟一个进程)
- 线程:程序中具体干事的人
浏览器是多线程的,当基于浏览器打开一个页面(开辟一个进程),会有不同的线程同时去做多件事情
- GUI渲染线程:用来渲染和解析HTML/CSS的 以及 绘制页面
- JS引擎线程:用来渲染和解析JS的
- HTTP网络线程:用来从服务器获取相关资源文件的(同源下,最多同时开辟5~7个HTTP线程)
- 定时器监听线程:监听定时器是否到时间的(计时的)
- 事件监听线程:监听事件是否触发的
当我们从服务器获取HTML代码后,浏览器会分配“GUI渲染线程”自上而下解析代码
- 遇到:分配一个新的“HTTP线程”去获取对应的CSS资源,GUI继续想下渲染(异步)
- 遇到:无需获取资源,但是GUI也不会去立即渲染CSS代码,防止渲染顺序错乱;会等到DOM结构渲染完成,访问的link等资源也获取到了,按照之前书写的顺序,一次渲染样式!
- 遇到@import:也需要去服务器获取资源(基于HTTP线程),但是这个操作会把“GUI线程”挂起,无法继续想下渲染,知道CSS资源获取到之后,GUI才会继续向下渲染(同步:阻碍GUI渲染)
- 遇到
:和link是一样的,也是异步操作,也会分配新的HTTP线程去获取图片资源,GUI继续想下渲染
- 遇到
- 资源获取后再分配JS引擎线程把JS代码先渲染了
- 都渲染完了,GUI再继续想下渲染
自上而下处理完成后,目前只是把页面中的DOM结构(节点),构建出对应的层级关系!而这就是DOM树!(触发DOMContentLoaded事件) DOM树生成后,等待CSS资源都获取到,此时按照CSS书写的顺序,一次渲染和解析CSS代码(GUI渲染线程),生成CSSDOM树:计算出每个节点具备的样式(含某些样式是继承过来的等) 把DOM树和CSSOM树合并在一起,生成渲染树! 按照当前可视窗口的大小,计算每一个节点在视图中的位置和大小 按照计算好的规则,一层层的进行绘制
-
我们最好把所有的CSS合并压缩为一个,只请求一次就把所有样式获取到即可;分多次请求,因为HTTP的并发限制和可能出现的网络拥堵等问题,导致并不如请求一次快!!
- CSS合并为一个
- JS合并为一个
- 雪碧图
- ...
-
尽可能不要使用@import导入式,因为它会阻碍GUI的渲染;如果CSS样式代码不是很多使用style内嵌式更好(尤其是移动端开发);但是如果代码很多,还是使用link外链式(但是最好把link放在中);
-
图片懒加载一定要处理:不要在第一次渲染页面的时候,让图片资源的请求去占用有限的HTTP线程以及宽带资源,优先本着CSS/JS资源获取;当页面渲染完成后,再去根据图片是否出现在视口中,来加载真实的图片;
-
关于< script>的优化
-
最好把< script>放在body的末尾,等待DOM结构加载完成,再去获取和解析JS(此时就额可以获取页面中的DOM元素了)
-
也可以基于事件监听去处理
- window.onload:等待页面中所有资源(含DOM结构/CSS/JS等资源)都加载完触发
- window.addEventListEner('DOMContentLoaded',function(){}):只需要等待DOM结构加载完就会触发,所以触发的时机比window.onload会早很多!!!
-
也可以给< script>设置 async 或者 defer 异步属性
- async 获取异步,渲染同步:遇到< script async>分配新的HTTP去获取资源,GUI会继续渲染;当资源获取之后,立即结束GUI渲染,让Js引擎线程去渲染解析JS;JS代码渲染完,再去执行GUI渲染!
- defer 获取异步,渲染异步:遇到< script defer>,分配HTTP去获取资源,此时GUI继续渲染;当DOM结构渲染完成,而且设置defer的JS资源也都获取到了,按照之前编写的JS顺序,依次渲染解析JS代码!
-
async的特点是:只要js代码获取到,就会立即执行,不管书写的先后顺序,适用于JS之间不存在依赖的时候;
defer的特点是:必须等待GUI以及所有设置defer的JS代码都获取到,再按照之前书写的顺序,依次渲染和解析,即实现了资源的同时异步获取,也可以保证JS代码之间的依赖关系!
-
加快DOM TREE的构建
- 减少HTML的层级嵌套
- 使用符合W3C规范的语义化标签
- ...
-
加快CSSOM TREE的构建
- 选择器层级嵌套不要过深(或者前缀不要过长)(选择器的渲染肾虚:从右到左)
- 减少CSS表达式的使用
- ... 操作DOM比较消耗性能:大部分性能都消耗在了“DOM的重排(回流 Reflow)和重绘(Repaint)” 页面第一次渲染,必然会出现一次Layout(回流)和Painting(重绘);第一次渲染完成后: 重排(回流):如果浏览器的视口大小发生改变 或者 页面中元素的位置大小发生改变 再或者 “DOM结构”发生变化(删除、新增元素或挪动位置) ...浏览器都需要重新计算节点在视口中(本层)的最新位置(也就是重新Layout),完成后再分层和重新绘制! ---> 此操作非常消耗性能,所以我们应该尽可能减少重排(回流)的次数 重绘:视口/元素的位置大小都不变,只是修改了一些基础样式(例如:背景颜色、文字颜色、透明度...),此时我们无需重新Layout,只需要重新Painting即可! ----> 重绘操作是必不可免的,只要想让页面第一次渲染完后还可以再改变,必然需要重绘;而且触发一次回流,也必然会经历重绘!
-
基于Vue/React/Angular等框架进行开发,我们基于“数据驱动视图渲染”,规避了直接操作DOM,我们只需要操作数据,框架内部帮助我们操作DOM(它们做了很多减少DOM重排的操作)!
-
读写分离
- 新版本浏览器中存在“渲染队列机制”:当前上下文代码执行过程中,遇到修改元素样式的操作,并不会立即去修改样式,而是把其挪至到渲染队列中;代码继续向下执行...当代码执行完成后,此时会把渲染队列中,所有修改样式的操作,统一执行一次(只触发一次重排);
- 但是在此过程中,如果遇到了获取元素样式的操作,则“刷新渲染队列”(也就是把目前队列中的操作执行一次),引发一次重排!!
- 把获取样式的操作和修改样式的操作分离开