预先知识
浏览器加载的时候是自上而下的,加载和渲染为同步进行
加载不会阻塞下载,解析会阻塞下载
js解析的时候会阻塞其他的加载
一般浏览器会在后面解析js文件,因为js中的代码很有可能改变dom树的结构
把浏览器加载页面的工作分给渲染引擎和javascript引擎
简易版流程:解析渲染部分
浏览器加载顺序:
1、解析到head标签内的内部样式表①,渲染引擎加载样式表中的样式,下载引用到的图片,下载等工作可以跟渲染同时进行。
2、向服务器请求链如样式表②,然后渲染引擎加载样式表中的样式。
3、下载链入js③,浏览器将控制权交给js引擎,js引擎解释执行。
4、遇到script标签,将控制权交给js引擎,从上到下,js引擎解释执行。
5、加载完head标签后,继续向下解析,执行到内部js⑤,js引擎解释执行。
6、加载完body中的标签,之后再将控制权交给js引擎,js引擎解释执行内部js⑥。
7 、遇到html结束标签,加载完毕,将构建好的DOM树和计算好的样式整合构成渲染树(render树),在浏览器窗口中画出。
js文件和script标签的加载 在解析到链入js文件或者script标签时。浏览器会阻塞其它下载,即无法并行解析和下载,在请求js文件时,浏览器会一直等待返回结果并解析完成之后才会继续向下执行。
重点:建议将script标签放在body的最后,因为js文件很有可能会修改DOM树和引用DOM元素,为了不让js文件解析执行时找不到要引用的DOM元素和DOM树被反复修改而带来的性能问题,所以最好将script标签放在body的最后,或者可以在js代码放在window.onload中,意思是等页面全部加载完之后再加载这些js代码。
页面全部渲染加载完毕之后,如果用户的某个操作引起了页面元素样式的变化就会引起重绘或者回流,只有颜色风格这样的样式改变时(比如background-color等)只会引起重绘,而当页面元素的尺寸,是否显示改变时,会引起回流
详细版流程
1、从浏览器接收url到开启网络请求线程(涉及到:浏览器机制,线程和进程之间的关系等)
2、开启网络线程到发出一个完整的http请求(涉及到:dns查询,tcp/ip请求,5层网络协议栈等)
3、从服务器接收到请求到对应后台接收到请求(涉及到:均衡负载,安全拦截,后台内部的处理等)
4、后台和前台的http交互(涉及到:http头,响应码、报文结构,cookie等,可以提下静态资源的cookie优化,以及编码解码如gzip压缩等)
5、缓存问题:http缓存(涉及到:涉及到http缓存头部,etag,expired,cache-control等)
6、浏览器接收到http数据包后的解析流程(涉及到:html的词法分析,然后解析成dom树,同时解析css生成css规则树,合并生成render树。然后layout布局、painting渲染、复合图层的合成、GPU绘制、外链接处理、loaded和documentloaded等)
7、css可视化格式模型(涉及到:元素渲染规则,如:包含块,控制框,BFC,IFC等概念)
8、js引擎解析过程(涉及到:js解释阶段,预处理阶段,执行阶段生成执行上下文,VO(全局对象),作用域链,回收机制等)
9、其他(扩展其他模块:跨域,web安全等)
从浏览器接收到url到开启网络请求线程
1、浏览器是多进程的
(1)浏览器是多进程的;
(2)不同类型的标签页会开启一个新的进程;
(3)相同类型的标签页会合并到一个进程中。
浏览器中各个进程以及作用:
1、浏览器进程:只有1个进程,(1)负责管理各个标签的创建和销毁;(2)负责浏览器页面显示;(3)负责资源的管理和下载;
2、第三方插件进程:可以是多个进程,负责每一个第三方插件的使用,每一个第三方插件使用时候会创建一个对应的进程;
3、GPU进程:最多1个进程,负责3D绘制和硬件加速;
4、浏览器渲染进程:可以是多个进程,浏览器的内核,每个tab页一个进程,主要负责HTML、,css,js等文件的解析,执行和渲染,以及事件处理等。
2、浏览器渲染进程(内核进程)
每一个tab页面是浏览器内核进程,然后这个每一个进程是多线程的,它有几大类子线程:
(1)GUI线程;(2)JS引擎线程;(3)事件触发线程;(4)定时器线程;(5)异步的http网络请求线程
可以看出来JS引擎是内核进程中的一个线程,所以常说JS引擎时单线程的。
3、解析URL
输入url后,会进行解析(URL是统一资源定位符)。
URL包括几个部分:(1)protocol,协议头,比如http,https,ftp等;(2)host,主机域名或者IP地址;(3)port,端口号;(4)path,目录路径;(5)query,查询的参数;(6)fragment,#后边的hash值,用来定位某一个位置。
4、网络请求时单独的线程
每一次网络请求都是需要单独开辟单独的线程进行,比如URL解析到http协议,就会新建一个网络线程去处理资源下载。
因此浏览器会根据解析出得协议,开辟一个网络线程,前往请求资源。
开启网络线程到发出一个完整的http请求
包括:DNS查询,tcp/ip请求构建,五层互联网协议等等。
1、DNS查询得到IP
如果输入的域名,需要DNS解析成IP,流程如下:
(1)浏览器有缓存,直接用浏览器缓存,没有就去本机缓存,没有就看是不是host。
(2)如果还没有,就向DNS域名服务器查询(这个过程经过路由,路由也有缓存),查询到对应的IP。
注意:1、域名查询的时候有可能经过CDN调度器(如果CDN有存储功能);
2、DNS解析是很耗时的,因此如果解析域名过多,首屏加载会变慢,可以考虑使用dns-prefetch优化。
2、tcp/ip请求构建
http的本质就是tcp/ip请求构建。需要3次握手规则建立连接,以及断开连接时候的4次挥手。
tcp将http长报文划分为短报文,通过3次握手与服务端建立连接,进行可靠的传输。
3次握手步骤:
客户端:hello,你是server么?服务端:hello,我是server,你是client么 客户端:yes,我是client 建立成功之后,接下来就是正式传输数据。
然后,等到断开连接时,需要进行4次挥手(因为是全双工的,所以需要4次握手)。
4次挥手步骤:
主动方:我已经关闭了向你那边的主动通道了,只能被动接收了 。被动方:收到通道关闭的信息 。被动方:那我也告诉你,我这边向你的主动通道也关闭了 。主动方:最后收到数据,之后双方无法通信
tcp/ip的并发限制
浏览器对同一域名下并发的tcp连接是有限制的(2-10个不等)。而且在http1.0中往往一个资源下载就需要对应一个tcp/ip请求。所以针对这个瓶颈,又出现了很多的资源优化方案。
get和post区别
get和post本质都是tcp/ip,但是除了http外层外,在tcp/ip层面也有区别。get会产生1个tcp数据包,post产生2个tcp数据包。
具体就是:
(1)get请求时,浏览器会把header和data一起发送出去,服务器响应200(返回数据)。
(2)post请求时,浏览器首先发送headers,服务器响应100 continue,浏览器再发送data,服务器响应200(返回数据)。
3、五层网络协议栈
客户端发出http请求到服务器接收,中间会经过一系列的流程。
客户端发送请求具体:从应用层发动http请求,到传输层通过三次握手简历tcp/ip连接,再到网络层的ip寻址,再到数据链路层的封装成帧,最后在物理层通过物理介质传输。
服务端接收请求具体:反过来。
五层网络协议:
1、应用层(DNS,HTTP):DNS解析成IP并发送http请求;
2、传输层(TCP,UDP):建立TCP连接(3次握手);
3、网络层(IP,ARP):IP寻址;
4、数据链路层(PPP):封装成帧;
5、物理层(利用物理介质传输比特流):物理传输(通过双绞线,电磁波等各种介质)。
其实也有一个完整的OSI七层框架,与之相比,多了会话层、表示层
OSI七层框架:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层
表示层:主要处理两个通信系统中交互信息的表示方式,包括数据格式交换,数据加密和解密,数据压缩和终端类型转换等。
会话层:具体管理不同用户和进程之间的对话,如控制登录和注销过程。
六、从服务器接收请求到对应后台接收到请求-可略
服务端接收到请求时,内部会有很多处理。
包括:均衡负载,
1、负载均衡
对于大型项目,并发访问很大,一台服务器吃不消,一般会有若干台服务器组成一个集群,然后配合反向代理实现均衡负载。均衡负载不止一种实现方式。
概括的说:用户发送的请求指向调度服务器(反向代理服务器,比如nginx的均衡负载),然后调度服务器根据实际的调度算法,分配不同的请求给对应的集群中的服务器执行,然后调度服务器等待实际服务器的HTTP响应,并且反馈给用户。
2、后台处理
一般后台都部署到容器中。过程如下:
(1)先是容器接收到请求(比如tomcat容器);
(2)然后对应容器中的后台程序接收到请求(比如java程序);
(3)然后就是后台自己的统一处理,处理完毕后响应结果。
具体概括一下:
(1)一般有的后端有统一的验证,比如安全拦截,跨域验证;
(2)如果不符合验证规则,就直接返回相应的http报文(拒绝请求等);
(3)如果验证通过了,才会进入到实际的后台代码,此时程序接收到请求,然后执行查询数据库,大量计算等等;
(4)等程序执行完毕后,会返回一个http响应包(一般这一步会经过多层封装);
(5)然后将这个数据包从后端返回到前端,完成交互。
七、后台和前台的http交互
前后端的交互,http报文作为信息的载体。
1.常见请求头、响应头
请求头:
响应头:
一般来说,请求头部和响应头部是匹配分析的。
比如:
(1)请求头部的Accept
要和响应头部的Content-Type
匹配,否则会报错;
(2)跨域请求中,请求头部的Origin
要匹配响应头的Access-Control-Allow-Origin
,否则会报跨域错误;
(3)使用缓存,请求头部的if-modified-since
,if-none-match
分别和响应头的Last-modified
,etag
对应。
2.响应实体中,就是服务端需要传给客户端的内容。
一般现在的接口请求时,实体中就是对应信息的json格式,而像页面请求这种,里面就是直接放一个html的字符串,然后浏览器自己解析并渲染。 3.cookie以及优化
cookie是浏览器的一种本地存储方式,一般用来帮助客户端和服务端通信的,常用来进行身份校验,结合服务端的session使用。
可能带来的问题-由于同域名会自动带上cookie
还可以再说http1.0、1.1、2.0区别
八、缓存问题:http缓存
http交互中,缓存很大程度上提升效率。
1、强缓存与弱缓存
缓存可以简单划分为两种类型:强缓存(200 from cache)与协商缓存(304);
区别简介一下:
对于协商缓存,可以使用ctrl + F5强制刷新,使得协商缓存无效。
对于强制缓存,在未过期,必须更新资源路径才能发送新的请求。
2、缓存头部简述
怎么在代码中区分强缓存和协商缓存?
通过不同的http的头部控制。
属于强制缓存的:
(http1.1)Cache-Control/Max-Age
(http1.0)Pragma/Expires
注意:cache_control的值:public,private,no-store,no-cache,max-age
属于协商缓存的:
(http1.1)If-None-Match/E-tag
(http1.0)If-Modified-Since/Last-Modified
再提一点,其实HTML页面中也有一个meta标签可以控制缓存方案-Pragma
<METAHTTP-EQUIV="Pragma"CONTENT="no-cache">
不过,这种方案还是比较少用到,因为支持情况不佳,譬如缓存代理服务器肯定不支持,所以不推荐。
3、缓存头部区别
在http1.1中,出现了一些新内容,弥补http1.0不足。
http1.0中的缓存控制:
(1)Pragma:严格来说不算缓存控制的头部,设置了no-cache会让本地缓存失效(属于编译控制,来实现特定的指令)。
(2)Expires:服务端配置,属于强缓存,用来控制在规定的时间之前,浏览器不会发送大量请求,而直接使用本地缓存,注意:Expires一般对应服务器端时间,比如:Expires:Fri, 30 Oct 1998 14:19:41
(3)If-Modified-Since/Last-modified:这两个是成对出现的,属于协商缓存。其中浏览器头部是If-Modified-Since,而服务端是Last-Modified,发送请求时,如果这两个匹配成功,代表服务器资源并没有改变,服务端不会返回资源实体,而是返回头部,告知浏览器使用本地缓存。Last-modifed指文件最后的修改时间,只能精确到1S以内。
http1.1中缓存的控制:
(1)cache-control :缓存的控制头部,有nocache,max-age等多个取值。
(2)Max-Age:服务端配置的,用来控制强缓存的,在规定的时间内,浏览器不用发出请求,直接使用本地的缓存。Max-Age是cache-control的值,比如:cache-control: max-age=60*1000,值是绝对时间,浏览器自己计算。
(3)If-None-Match/E-tag:这两个是成对的出现的,属于协商缓存,其中浏览器头部是If-None-Match,而服务端是E-tag,同样,发出请求后,如果If-None-Match和E-tag匹配,代表内容没有变化,告诉浏览器使用本地缓存,和Last-modified不同,E-tag更精确,它类似于指纹一样,基于FileEtag INode Mtime Size生成的,就是说文件变,指纹就会变,没有精确度的限制。
Cache-Control相比Expires?
1、都是强制缓存。
2、Expires使用服务端时间,因为存在时区,和浏览器本地时间可以修改问题,在http1.1不推荐使用Expires;Cache-Control的Max-Age是浏览器端本地的绝对时间。
3、同时使用Cache-Control和Expires,Cache_control优先级高。
E-tag相比Last-Modified?
1、都是协商缓存。
2、Last-modified指的是服务端文件最后改变时间,缺陷是精确只能到1s,文件周期性的改变,导致缓存失效;E-tag是一种指纹机制,文件指纹,只要文件改变,E-tag立刻变,没有精度限制。
3、带有E-tag和Last-modified时候,E-tag优先级高。
九、解析页面流程
前面提到是http交互,接下来是浏览器获取到html,然后解析,渲染。
1、流程简述
浏览器内核拿到内容后,渲染大致分为以下几步:
(1)解析html,构建DOM树;同时解析CSS,生成CSS规则树。
(2)合并DOM树和CSS规则树,生成Render树。
(3)布局Render树(layout/reflow),负责各元素的尺寸,位置计算。
(4)绘制render树(paint),绘制页面像素信息。
(5)浏览器会将各层的信息发给GPU。GPU会将各层合成(composite),显示在屏幕上。
2、html解析,构建DOM
这一步的流程是这样的:浏览器解析HTML,构建DOM树。实际上,稍微展开一下。
解析html到构建dom过程简述如下:
Bytes -> characters -> tokens -> nodes ->DOM 比如,有这样一个html页面:
浏览器的处理如下:
列举一下其中一些重点过程:
作者:豆皮范儿
链接:www.zhihu.com/question/30…
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
\
3、css解析,构建css规则树
CSS规则树的生成也是类似
Bytes→ characters → tokens → nodes → CSSOM
\
比如:style.css内容如下:
body { font-size: 16px}
p { font-weight: bold }
span { color: red }
p span { display: none }
img { float: right }
\
最终的CSSOM树就是
3、css解析,构建css规则树
CSS规则树的生成也是类似
Bytes→ characters → tokens → nodes → CSSOM
\
比如:style.css内容如下:
body { font-size: 16px}
p { font-weight: bold }
span { color: red }
p span { display: none }
img { float: right }
最终的CSSOM树就是
5、渲染
有了render树,接下来就是开始渲染,基本流程如下:
(1)计算CSS样式 ;
(2)构建渲染树 ;
(3)布局,主要定位坐标和大小,是否换行,各种position overflow z-index属性 ;
(4)绘制,将图像绘制出来。
然后,图中的线与箭头代表通过js动态修改了DOM或CSS,导致了重新布局(Layout)或渲染(Repaint)
这里Layout和Repaint的概念是有区别的:
(1)Layout,也称为Reflow,即回流。一般意味着元素的内容、结构、位置或尺寸发生了变化,需要重新计算样式和渲染树。
(2)Repaint,即重绘。意味着元素发生的改变只是影响了元素的一些外观之类的时候(例如,背景色,边框颜色,文字颜色等),此时只需要应用新样式绘制这个元素就可以了。
回流的成本开销要高于重绘,而且一个节点的回流往往回导致子节点以及同级节点的回流, 所以优化方案中一般都包括,尽量避免回流。
6、什么引起回流
1.页面渲染初始化
2.DOM结构改变,比如删除了某个节点
3.render树变化,比如减少了padding
4.窗口resize
5.最复杂的一种:获取某些属性,引发回流, 很多浏览器会对回流做优化,会等到数量足够时做一次批处理回流, 但是除了render树的直接变化,当获取一些属性时,浏览器为了获得正确的值也会触发回流,这样使得浏览器优化无效,包括
回流一定伴随着重绘,重绘却可以单独出现。
优化方案:
(1)减少逐项更改样式,做好一次性更改样式。或者将样式定义为class,并一次性更新。
(2)避免循环操作dom,创建一个documentFragment或div,在他上面进行所有的dom操作,最后添加到window.document中。
(3)避免多次读取offset等属性,无法避免就将他们缓存到变量中。
(4)将复杂的元素绝对定位或者固定定位,使他们脱离文档流,否则回流代价很高。
注意:改变字体大小会引起回流。
7、简单层和复合层
上述中的渲染中止步于绘制,但实际上绘制这一步也没有这么简单,它可以结合复合层和简单层的概念来讲。
简单介绍下:
(1)可以默认只有一个复合层,所有的DOM节点都是在这个复合图层下。
(2)如果开启了硬件加速功能,可以将某一个节点变成复合图层。
(3)复合图层之间的绘制互不干扰,直接GPU直接控制。
(4)简单图层中,就算是absolute等布局,变化时不影响整体回流,但是由于在同一个图层中,仍然会影响绘制的,因此做动画时候性能仍然很低。而复合层是独立的,所以一般做动画推荐使用硬件加速。
8、Chrome的调试
Chrome的开发者工具中,Performance中可以看到详细的渲染过程:
9、资源外链的下载
上面介绍了html解析,渲染流程。但实际上,在解析html时,会遇到一些资源连接,此时就需要进行单独处理了。
简单起见,这里将遇到的静态资源分为一下几大类(未列举所有):
- 遇到外链的处理
- css 样式资源
- js 脚本资源
- img 图片类资源
以下针对每种情况进行详细说明:
1. 遇到外链的处理
当遇到上述的外链时,会单独开启一个下载线程去下载资源(http1.1 中是每一个资源的下载都要开启一个 http 请求,对应一个 tcp/ip 链接)
2.遇到 css 样式资源
css 资源处理特点:
(1)css 下载时异步的,不会阻塞浏览器构建 DOM 树;
(2)但是会阻塞渲染,也就是在构建 render 树时,等到 css 下载解析后才进行(与浏览器优化有关,防止 css 规则不断变化,避免重复的构建)
(3)有例外,遇到 media query 声明的 css 是不会阻塞构建 render
3.遇到 js 脚本资源
JS 脚本资源的处理有几个特点:
(1)阻塞浏览器的解析,也就是说发现一个外链脚本时,需等待脚本下载完成并执行后才会继续解析 HTML。
(2)浏览器的优化,一般现代浏览器有优化,在脚本阻塞时,也会继续下载其它资源(当然有并发上限),但是虽然脚本可以并行下载,解析过程仍然是阻塞的,也就是说必须这个脚本执行完毕后才会接下来的解析,并行下载只是一种优化而已。
(3)defer 与 async,普通的脚本是会阻塞浏览器解析的,但是可以加上 defer 或 async 属性,这样脚本就变成异步了,可以等到解析完毕后再执行。
注意,defer 和 async 是有区别的:defer 是延迟执行,而 async 是异步执行。
简单的说:
(1)async是异步执行,异步下载完毕后就会执行,不确保执行顺序,一定在onload前,但不确定在DOMContentLoaded事件的前或后。
(2)defer是延迟执行,在浏览器看起来的效果像是将脚本放在了body后面一样(虽然按规范应该是在DOMContentLoaded事件前,但实际上不同浏览器的优化效果不一样,也有可能在它后面)。
4.遇到img图片类资源
遇到图片等资源时,直接就是异步下载,不会阻塞解析,下载完毕后直接用图片替换原有src的地方
10、loaded和domcontentloaded
对比:
(1)DOMContentLoaded 事件触发时,仅当DOM加载完成,不包括样式表,图片(譬如如果有async加载的脚本就不一定完成)。
(2)load 事件触发时,页面上所有的DOM,样式表,脚本,图片都已经加载完成了。