浏览器的底层运行机制
首先,按照惯例,依旧是一波小小的复习
一、复习
1.进程和线程
-
进程:指一个程序(或浏览器打开一个页面就是开辟一个进程)
-
线程:程序中具体执行任务的一个进程中可能包含一到多个线程!!
-
单线程:同时只能处理一件事,上一件事处理完,下一件事才能处理「同步编程」
-
多线程:可同时处理多件事情,上一件事情即便没有处理完,下一件事也可以交给其他线程去处理「异步编程」
-
2.浏览器的渲染进程
浏览器打开一个页面就是开辟一个进程,每个进程中会包含多个线程「浏览器是多线程的」!!
-
GUI渲染线程:渲染解析HTML/CSS代码,并且绘制页面 =>W3C
-
JS引擎线程:渲染解析JS代码 =>ECMAScript「ECMA」
-
定时器监听线程:设置定时器后,浏览器就会分配一个线程(小人)去监听定时器是否到时间
-
事件监听线程:完成事件绑定后,也会分配一个线程去监听事件是否触发
-
HTTP网络线程:分配一个线程,去服务器获取相关的资源信息 「并发上限:同源下最多开辟5~7个」
-
...
接下来正式开始!
二、浏览器底层渲染机制
当基于HTTP网络,从服务器端获取到页面源代码后,到浏览器把其绘制成页面,中间所做的事情,就是我们要研究的渲染机制!
1. 创建DOM TREE(DOM树)
GUI首先渲染所有的HTML结构,计算出结构之间的层级关系
触发DOMContentLoaded事件:window.addEventListener('DOMContentLoaded',function(){...})
2. 创建CSSOM TREE(样式树)
样式树:渲染解析CSS完毕后,GUI按照原先导入的顺序,依次解析CSS代码,记录了相应节点应该具备的样式「包括继承的样式、或者根据优先级重构的样式等等」
3. 生成RENDER TREE「渲染树」
把DOM TREE 和 CSSOM TREE 合并在一起生成RENDER TREE
渲染树中详细记录了DOM的层级结构和每个节点应该具备的样式!!
4. Layout布局
根据可视窗口大小,按照RENDER TREE,计算出每个节点在视口中的位置和排版等「含:位置、方向、大小、排列方式...」
5. 分层
规划出页面各个文档流(层面)中,所有节点的具体绘制步骤!!
6. Painting绘制
GUI根据上一步计算出来的步骤,开始绘制页面即可!!
触发load事件:window.onload=function(){...}
三、渲染页面流程的步骤
-
打开页面,浏览器首先分配一个“HTTP线程”去服务器获取HTML资源;
-
获取资源后,分配“GUI渲染线程”自上而下,逐行渲染解析HTML代码!
-
渲染HTML/CSS代码:遵循W3C规则,GUI渲染线程去处理
-
渲染JS代码:遵循ECMAScript(ECMA-262)规则,JS引擎线程去处理
-
在渲染代码过程中,如果遇到 link/img/script[src=xxx]/audio/video 等,浏览器需要开辟“HTTP线程”,从服务器端获取到对应的资源文件(文件中的代码)
-
1.遇到<link>
-
浏览器分配一个“HTTP线程”去服务器获取CSS样式
-
同时GUI会继续向下渲染「异步」
-
等待HTML渲染解析完毕「DOM TREE已经构建」,并且CSS样式“都已经”获取到了,再按照之前导入样式的顺序,逐一渲染解析CSS样式「创建CSSOM TREE」!
-
目的:确保CSS优先级(层级)的准确,所以渲染顺序需要和之前编写的导入顺序一致
-
如果某个资源一直没有响应,或者响应结果是获取失败,则忽略它的渲染!
-
1.1link三种 CSS 样式的渲染区别
-
渲染过程中遇到 则开辟新的HTTP线程去获取资源文件,GUI不受影响,继续向下渲染“异步” 【代码多的时候 用link】
-
如果遇到的是这个标签,则无需获取资源,代码本身就有,此时GUI直接去渲染即可“同步” 【代码少的时候 最优】
-
如果遇到的是@import ,也会开辟新的HTTP线程去获取资源,但是此时会阻碍GUI的渲染,只有当样式资源获取后,GUI才会渲染新拿到的样式代码 “同步” 【影响页面的渲染速度】
1.2优化技巧
-
如果CSS样式“代码比较少”,我们直接使用内嵌式即可(尤其是移动端,我们经常这么干);
-
但是如果代码比较多,还用内嵌式则会导致HTML请求速度都很慢,此时我们使用外链式;除特殊必要,不建议使用导入式;
2.遇到 <style>
- 内嵌样式:获取HTML的时候,就把嵌入的样式也都获取到了!不需要再开辟HTTP线程,去服务器获取资源了!
- 为了保证CSS渲染的顺序和样式优先级,此时GUI也不会立即渲染解析CSS代码,还是要等待DOM树构建完毕、其它CSS资源都获取到,再按照顺序进行解析!!
-
<style>
方式确保:在DOM TREE构建完毕后,可以立即拿到样式代码去渲染解析!
3.遇到 import 导入式
-
也会开辟新的HTTP线程去服务器获取资源,但是其“会阻碍GUI继续向下渲染”,也就是GUI此时停止工作;当把资源从服务器获取后,GUI才能继续向下渲染
-
这种模式是同步的
-
真实项目中,尽可能不要使用@import「除非在less/sass/stylus等预编译语言中」
4.遇到 img/audio/video
-
开辟新的HTTP线程去获取相关资源,GUI继续向下渲染
-
当浏览器去绘制页面的时候,如果资源已经获取,此时再去渲染
-
图片懒加载:加快页面第一次渲染的速度
-
不让图片资源占用有并发上限的HTTP线程
-
第一次渲染页面,不去渲染耗时间的图片
-
5.遇到 <script src="">
- 默认是同步的:默认会阻碍GUI的渲染:首先开辟“HTTP线程”去服务器获取JS资源,此时GUI停止渲染,等待资源获取完毕,“JS引擎线程”把代码都渲染解析好了,GUI才会继续渲染!!
5.1存在的问题
-
如果把
<script>
放在HEAD中导入,则JS代码内是无法获取DOM元素的{原因:其阻碍GUI渲染,导致JS代码执行的时候,DOM TREE还未构建} -
所以我们一般都是把其放在BODY末尾导入,一方面不让其阻碍DOM TREE的构建,另一方面也是要等待DOM TREE构建完毕,我们可以在JS中获取DOM元素!
-
当然也可以监听事件「DOMContentLoaded && load」,在事件中获取DOM元素
5.2依然放在HEAD中,还想能拿到DOM元素,该如何处理?
-
把获取元素的代码放在window.onload事件中「等待所有资源渲染完毕后触发」
-
还可以给
<script>
设置 async 或者 defer 属性 -
设置了async属性:
-
获取资源是异步的:浏览器分配HTTP线程获取JS资源,此时不阻碍GUI,GUI会继续渲染
-
代码执行是同步的:当JS资源获取后,立即执行JS代码,阻碍GUI正在做的事情!
-
特点:多个
<script>
设置async,最后也都是谁先获取先执行谁,没有所谓的顺序概念,一般用于不需要相互依赖的JS中!!
-
-
设置defer属性:
-
和
<link>
非常的类似,获取和渲染代码都是异步的。 -
开辟HTTP线程去获取JS资源,此时GUI继续渲染,等待GUI渲染完毕「DOM TREE生成」,而要获取的JS资源也都拿到了,再按之前导入JS的顺序,依次渲染解析JS
-
特点:多个
<script>
设置defer,它要等待所有资源都获取后,按照导入的先后顺序,依次渲染解析JS!!
-
总结
真实项目中,要不然就是把<script>
放在末尾,要不然就是设置async/defer;在不考虑多个JS之间执行的先后顺序「也就是相互之间没有依赖」,可以设置async,谁先获取就先执行谁!反之,需要有顺序和依赖,则设置defer!
四、总结优化技巧
学习底层机制,目的是进行“前端性能优化”,写出更高质量的代码!!
优化思想:CRP「关键渲染路径」
-
减少使用@import,
<script>
放在末尾或者设置async/defer让其变为异步处理,不让他们阻碍GUI干活!! -
使用语义化标签、层级结构不要过深!
-
CSS选择器前缀不要过长 「选择器是从右到左渲染的」
-
样式代码较少的情况下,使用内嵌式!代码较多,采用外链式;坚决不用导入式!
-
CSS合并压缩为1个、JS也合并压缩为1个,减少HTTP请求的数量;
- 对于图片等资源,做延迟加载「等待页面彻底渲染完毕,在去获取渲染」;
-
减少DOM的回流(重排)和重绘!
-
...
转载自:https://juejin.cn/post/7174080156422635551