URL,它的组成:
- 协议
- 服务器地址(域名或IP+端口)
- 路径
- 文件名
比如:https://www.baidu.com/index.html
其中
- https://是一种协议 当然,HTTP也是 ftp也是...
- www.baidu.com是服务器地址,当然你知道百度的IP也可以,例如我用ping命令得到百度的ip
14.215.177.39,那么我可以用http://14.215.177.39打开百度
3.index.html包含了路径和文件名,当然通常index.html是可以省略的,所以你打开百度时,并没有看到这个。
DNS
DNS:Domain Name Server,域名服务器。
是进行域名(domain name)和与之相对应的IP地址 (IP address)转换的服务器。DNS中保存了一张域名(domain name)和与之相对应的IP地址 (IP address)的表,以解析消息的域名。
在平时我们进行开发时,后端提供的接口地址通常是有IP地址加上端口号(8080什么鬼的)组成的,但是当我们把网站发布出去时,通常都需要把IP改成用域名。用域名去映射IP地址主要是为了简化记忆,比如百度的地址是132.21.33.221:8766,但有了域名baidu.com ,对于用户来说是不是一下子就能记住了呢?
因此,当用户在浏览器输入https://www.baidu.com回车时,它经历了以下步骤:
- 浏览器根据地址去本身缓存中查找dns解析记录,如果有,则直接返回IP地址,否则浏览器会查找操作系统中(hosts文件)是否有该域名的dns解析记录,如果有则返回。
- 如果浏览器缓存和操作系统hosts中均无该域名的dns解析记录,或者已经过期,此时就会向域名服务器发起请求来解析这个域名。
- 请求会先到LDNS(本地域名服务器),让它来尝试解析这个域名,如果LDNS也解析不了,则直接到根域名解析器请求解析
- 根域名服务器给LDNS返回一个所查询余的主域名服务器(gTLDServer)地址。
- 此时LDNS再向上一步返回的gTLD服务器发起解析请求。
- gTLD服务器接收到解析请求后查找并返回此域名对应的Name Server域名服务器的地址,这个Name Server通常就是你注册的域名服务器(比如阿里dns、腾讯dns等)
- Name Server域名服务器会查询存储的域名和IP的映射关系表,正常情况下都根据域名得到目标IP记录,连同一个TTL值返回给DNS Server域名服务器
- 返回该域名对应的IP和TTL值,Local DNS Server会缓存这个域名和IP的对应关系,缓存的时间有TTL值控制。
- 把解析的结果返回给用户,用户根据TTL值缓存在本地系统缓存中,域名解析过程结束。
HTTP请求发起和响应
如果是为了讲网络通信,说完上面那些就差不多了。但今天要整理的是web应用中请求的发起和响应以及页面渲染的原理,因此以上只是铺垫。
在一个web程序开发中,一般都有前端和后端之分,前端负责向后端请求数据和展示页面,后端负责接收请求和做出响应发回给前端,他们之间的协作的桥梁是什么呢?
是API
API是什么?不就是一个URL吗?
URL又是啥呢?上面说到就是HTTP连接的一种具体的载体
因此,
无论对于前端或者是后端,理解HTTP,无论是对自身对编程的理解,还是和同事协作,都是好处大大的,
下面,根据上面各个知识点的理解,我们来整理一下并解决一下上面提到的第一个问题:
从用户输入URL,到浏览器呈现给用户页面,经过了什么过程?
- 用户输入URL,浏览器获取到URL
- 浏览器(应用层)进行DNS解析(如果输入的是IP地址,此步骤省略)
- 根据解析出的IP地址+端口,浏览器(应用层)发起HTTP请求,请求中携带(请求头header(也可细分为请求行和请求头)、请求体body),
header包含:
- 请求的方法(get、post、put..)
- 协议(http、https、ftp、sftp...)
- 目标url(具体的请求路径已经文件名)
- 一些必要信息(缓存、cookie之类)
body包含:
- 请求的内容
- 请求到达传输层,tcp协议为传输报文提供可靠的字节流传输服务,它通过三次握手等手段来保证传输过程中的安全可靠。通过对大块数据的分割成一个个报文段的方式提供给大量数据的便携传输。
- 到网络层, 网络层通过ARP寻址得到接收方的Mac地址,IP协议把在传输层被分割成一个个数据包传送接收方。
- 数据到达数据链路层,请求阶段完成
- 接收方在数据链路层收到数据包之后,层层传递到应用层,接收方应用程序就获得到请求报文。
- 接收方收到发送方的HTTP请求之后,进行请求文件资源(如HTML页面)的寻找并响应报文
- 发送方收到响应报文后,如果报文中的状态码表示请求成功,则接受返回的资源(如HTML文件),进行页面渲染。
页面的渲染
当一个请求的发起和响应都完成之后,浏览器就会收到响应内容,但浏览器收到的是一串串的代码或URL链接,怎么把这些代码转化成用户可以看得懂的界面呈现出来,就是浏览器的工作了。
目前市场上的浏览器已经不下百种,各个浏览器根据内核又可以分成几大类,每一类浏览器对页面的渲染原理和过程有所差异。
但总的来说,各个浏览器渲染页面都基本遵循如下图的流程:
图中有几处英文词汇可能不好理解,没关系,先做一下解释:
- HTML parser:HTML解析器,其本质是将HTML文本解释成DOM tree。
- CSS parser:CSS解析器,其本质是讲DOM中各元素对象加入样式信息
- JavaScript引擎:专门处理JavaScript脚本的虚拟机,其本质是解析JS代码并且把逻辑(HTML和CSS的操作)应用到布局中,从而按程序要的要求呈现相应的结果
- DOM tree:文档对象模型树,也就是浏览器通过HTMLparser解析HTML页面生成的HTML树状结构以及相应的接口。
- render tree:渲染树,也就是浏览器引擎通过DOM Tree和CSS Rule Tree构建出来的一个树状结构,和dom tree不一样的是,它只有要最终呈现出来的内容,像<head>或者带有display:none的节点是不存在render tree中的。
- layout:也叫reflow 重排,渲染中的一种行为。当rendertree中任一节点的几何尺寸发生改变了,render tree都会重新布局。
- repaint:重绘,渲染中的一种行为。render tree中任一元素样式属性(几何尺寸没改变)发生改变了,render tree都会重新画,比如字体颜色、背景等变化。
所以,根据关键词汇的解释以及顺着流程图的流程,可以总结出,浏览器解析渲染页面主要包括以下过程:
- 浏览器通过HTMLParser根据深度遍历的原则把HTML解析成DOM Tree。
- 将CSS解析成CSS Rule Tree(CSSOM Tree)。
- 根据DOM树和CSSOM树来构造render Tree。
- layout:根据得到的render tree来计算所有节点在屏幕的位置。
- paint:遍历render树,并调用硬件图形API来绘制每个节点。
前端性能优化
对于页面渲染基本上这样就是一个的流程,看完之后,有没有什么感觉在实际编码中可以优化的点呢?没有吧?因为很多细节都没有讲述,因此为了找到可优化的点,在此对页面渲染过程的几个关键步骤做一下陈述:
1. HTML解析:
上面讲到,HTML解析是浏览器的HTML解析器把HTML解析成dom tree,而在解析过程,浏览器根据HTML文件的结构从上到下解析html,HTML元素是以深度优先的方式解析,而script、link、style等标签会使解析过程产生阻塞,阻塞的情况有:
- 外部样式会阻塞内部脚本的执行。
- 外部样式与外部脚本并行加载,但外部样式会阻塞外部脚本执行。
- 如果外部脚本带有async属性,则外部脚本的加载与执行不受外部样式影响
- 如果link标签是动态创建(js生成),不管有无async属性,都不会阻塞外部脚本的加载与执行。
2. CSS解析:
CSS Parser作用就是将很多个CSS文件中的样式合并解析出具有树形结构Style Rules,在对样式解析的过程中,默认CSS选择器是从右往左进行解析的。至于为什么是从右到左,而不是从左到右、也是不会从左到左...
下面举个栗子来说一下:
假如现在有这样的一个样式:
#parent .ch1 .dh1 {}
.fh1 .ch1 .dh1{}
.ah1 .ch1 .eh1 {}
#parent .fh1 {}
.ch1 .dh1{}
下面是从左到右和从右到左的对比:
从两个图的比较就可以看几点:
- 右边的tree复杂度要比左边的低
- 右边的tree公用样式重合度比左边的低
- 右边的tree从根开始的节点数要比左边的少
可能光看这几点没看出什么问题,但你要知道:浏览器中的css解析器负责css的解析,并为每个节点计算出样式,因此虽然css解析器要做的事情不多,但要每个节点都要进行遍历查找计算,计算量极大,因此解析的方式是决定其性能的关键点。
就如:
#parant .a{}
和
.a{}
估计绝大多数人都会认为前者要比后者性能更优,其实不然,在解析过程中
#paran .a{}意味着css解析器要先找到#parent再找到他下面的.a所在节点
而后者可以直接定位到.a{}因此哪一种方式更优,显而易见。
3. 脚本执行:
浏览器解析HTML时,当遇到<script>标签就会立即解析脚本,同时阻塞解析文档直到脚本执行完毕(你可能问为什么要这样设计,明显啊,脚本的执行是改变css和dom,会造成render tree不停的重绘和重排的),而当<script>是引入外部js文件时,会阻塞到js文件下载完成并且执行完成为止(除非加了defer或者async属性)。脚本在解析过程中将对dom或css的操作解析出来加入到DOM Tree和cssom中。
性能优化
把这些度讲完之后,对于性能优化的点,相信大家心里都有点X数了吧,下面简单总结一下日常开发过程中常用的性能优化的地方:
1.对于css:
- 优化选择器路径:健全的css选择器固然是能让开发看起来更清晰,然后对于css的解析来说却是个很大的性能问题,因此相比于 .a .b .c{} ,更倾向于大家写.c{}。
- 压缩文件:尽可能的压缩你的css文件大小,减少资源下载的负担。
- 选择器合并:把有共同的属性内容的一系列选择器组合到一起,能压缩空间和资源开销
- 精准样式:尽可能减少不必要的属性设置,比如你只要设置{padding-left:10px}的值,那就避免{padding:0 0 0 10px}这样的写法
- 雪碧图:在合理的地方把一些小的图标合并到一张图中,这样所有的图片只需要一次请求,然后通过定位的方式获取相应的图标,这样能避免一个图标一次请求的资源浪费。
- 避免通配符:.a .b *{} 像这样的选择器,根据从右到左的解析顺序在解析过程中遇到通配符(*)回去遍历整个dom的,这样性能问题就大大的了。
- 少用Float:Float在渲染时计算量比较大,尽量减少使用。
- 0值去单位:对于为0的值,尽量不要加单位,增加兼容性
2.对于JavaScript:
- 优化选择器路径:健全的css选择器固然是能让开发看起来更清晰,然后对于css的解析来说却是个很大的性能问题,因此相比于 .a .b .c{} ,更倾向于大家写.c{}。
- 压缩文件:尽可能的压缩你的css文件大小,减少资源下载的负担。
- 选择器合并:把有共同的属性内容的一系列选择器组合到一起,能压缩空间和资源开销
- 精准样式:尽可能减少不必要的属性设置,比如你只要设置{padding-left:10px}的值,那就避免{padding:0 0 0 10px}这样的写法
- 雪碧图:在合理的地方把一些小的图标合并到一张图中,这样所有的图片只需要一次请求,然后通过定位的方式获取相应的图标,这样能避免一个图标一次请求的资源浪费。
- 避免通配符:.a .b *{} 像这样的选择器,根据从右到左的解析顺序在解析过程中遇到通配符(*)回去遍历整个dom的,这样性能问题就大大的了。
- 少用Float:Float在渲染时计算量比较大,尽量减少使用。
- 0值去单位:对于为0的值,尽量不要加单位,增加兼容性。
- 尽可能把script标签放到body之后,避免页面需要等待js执行完成之后dom才能继续执行,最大程度保证页面尽快的展示出来。
- 尽可能合并script代码,
- css能干的事情,尽量不要用JavaScript来干。毕竟JavaScript的解析执行过于直接和粗暴,而css效率更高。
- 尽可能压缩的js文件,减少资源下载的负担
- 尽可能避免在js中逐条操作dom样式,尽可能预定义好css样式,然后通过改变样式名来修改dom样式,这样集中式的操作能减少reflow或repaint的次数。
- 尽可能少的在js中创建dom,而是预先埋到HTML中用display:none来隐藏,在js中按需调用,减少js对dom的暴力操作。
3. 对于HTML:
- 避免再HTML中直接写css代码。
- 使用Viewport加速页面的渲染。
- 使用语义化标签,减少css的代码,增加可读性和SEO。
- 减少标签的使用,dom解析是一个大量遍历的过程,减少无必要的标签,能降低遍历的次数。
- 避免src、href等的值为空。
- 减少dns查询的次数。