页面请求到响应的过程(附带浏览器性能优化)

200 阅读11分钟

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情

当用户在浏览器地址栏中输入网址,到看到页面,经历的步骤有那些?

1.解析输入的URL地址

www.baidu.com:443/index.html

  • 传输协议(把信息在客户端和服务器端进行传递,类似于快递小哥)

    • http 超文本传输协议(传递的内容除了文本,还有可能是其他类型,二进制编码、BASE64码、文件流等等)
    • https 比HTTP更加安全的传输协议(传输通道设置加密算法SSL),一般支付类网站都是HTTPS协议
    • ftp 资源上传协议,一般应用于把文件直接上传到服务器端
  • 域名 baidu.com

    • 一级域名 www.baidu.com

      • 二级域名 image.baidu.com
    • 三级域名 www.image.baidu.com

    • 常用域名性质: .com国际 / .cn中国 / .gov政府 / .org官方 / .net系统 / .io博客 / .vip ....

  • 端口号(根据端口号:找到当前服务器上指定的服务)

    • 0-65535之间
    • 不同协议有自己默认的端口号(也就是自己不用写,浏览器会帮我们加上)
      • http => 80
      • https => 443
      • ftp => 21
      • 除这几个在书写的时候可以省略,其余的不能省
  • 请求资源的路径和名称

  • /index.html

    • 一般情况下,如果我们访问的是index.html等,可以省略不写(因为服务端一般会设置index.html为默认文档,当然也可以自定义)
    • 伪URL
    • SEO优化
    • 数据请求的接口地址 /user/list
//=> 数据请求的接口地址  /user/list
静态地址
https://item.jd.com/10789.html(搜索引擎抓取)
    
动态页面地址:真实地址 
https://item.jd.com/detail.php?id=10789 
需要把这样的地址重写为上述的静态地址(URL伪重写技术)
  • 问号传参部分 ?xxx=xxx

  • 客户端基于GET系列请求,把信息传递回服务器,一般都会基于问号传参的模式

    • 页面之间跳转,信息的一些通信也可以基于问号传参的方式(单页面组件和组件跳转之间的信息通信,也可能基于问号传参)
    • 关于传递的内容需要进行编码处理(处理特殊字符和中文)
      • encodeURI / decodeURI
      • encodeURIComponent / decodeURIComponent
      • escape / unescape
      • ....
/* 
 * encodeURI / decodeURI:只能把空格和中文内容进行编码和解码,所以一般应用于这种
 模式处理整个URL的编码
 * encodeURIComponent / decodeURIComponent:会把所有的特殊字符和汉字都进行编码
 和解码,一般不会整个URL编码,只会给传递的每一个参数值单独编码
 * 
 * =>客户端和服务器端都支持这两种编码和解码方式(客户端和服务器端信息编码一般都是
 基于这种方式)
 */
 
 let str = `https://www.baidu.com?lx=aa&url=${encodeURIComponent('htt
 ps://www.jd.com')}&name=${encodeURIComponent("买买买")}`;
 console.log(str);
 
// ========= ========= ========= ========= ========= ========= =========
 
 let str ='https://www.baidu.com?lx=aa&url=https://www.baidu.com&name
 ="买买买"';
 console.log(encodeURI(str));
 //=> https://www.baidu.com?lx=aa&url=https://www.baidu.com&name=%22%E4%B9%B0%E4%B9%B0%E4%B9%B0%22
 
 // ========= ========= ========= ========= ========= ========= =========
 
 /* 
 * escape / unescape:这种方式不一定所有的后台都有,所以一般只应用于客户端自己
 内部编码,例如:存储cookie信息,把存储的中文进行编码和解码;特殊符号也会被编码;
 */
console.log(escape("买买买")); //=> '%u4E70%u4E70%u4E70'
console.log(unescape("'%u4E70%u4E70%u4E70'")); //=> '买买买'
​
一次完整的HTTP事务
1. 建立起TCP/HTTP传输通道(客户端和服务器端)
2. 客户端正常发送请求
3. 服务器端正常把信息返回
4. 断开TCP/HTTP传输通道
  • 设置哈希HASH #xxx

2. DNS解析

  • 网站中,每发送一个TCP请求,都要进行DNS解析(一旦当前域名解析过一次,浏览器一般会缓存解析记录,缓存时间一般在1分钟左右,后期发送的请求如果还是这个域名,则跳过解析步骤 =>这是一个性能优化点)

  • 真实项目中,一个大型网站,它要请求的资源是分散到不同的服务器上的(每一个服务器都会有自己的一个域名解析)

  • WEB 服务器(处理静态资源文件,例如:等html/css/js的请求)

    • 数据服务器(处理数据请求)
    • 图片服务器(处理图片请求)
    • 音视频服务器
    • .....
    • 这样导致,我们需要解析的DNS会有很多次
  • 优化技巧:DNS Prefetch 即 DNS 预获取

让页面加载(尤其是后期资源的加载)更顺畅更快一些

<link rel="dns-prefetch" href="//static.360buyimg.com">
<link rel="dns-prefetch" href="//misc.360buyimg.com">
<link rel="dns-prefetch" href="//img10.360buyimg.com">
<link rel="dns-prefetch" href="//img11.360buyimg.com">
<link rel="dns-prefetch" href="//img12.360buyimg.com">

3.基于TCP的三次握手,构建客户端和服务器端的连接通道

  • 只有建立好连接通道,才能基于HTTP等传输协议,实现客户端和服务器端的信息交互
  • 第一次握手:由浏览器发起,告诉服务器我要发送请求了
  • 第二次握手:由服务器发起,告诉浏览器我准备接收了,你赶紧发吧
  • 第三次握手:由浏览器发送,告诉服务器,我马上就发了,准备接收吧

4.发送HTTP请求

基于HTTP等传输协议,客户端把一些信息传递给服务器

  • HTTP请求报文(所有客户端传递给服务器的内容,统称为请求报文)
    • 谷歌控制台NetWork可以查看
    • 请求起始行
    • 请求首部(请求头)
    • 请求主体
  • 强缓存 和 304协商缓存(性能优化:减少HTTP请求的次数
    • 强缓存(Cache-Contorol 和 Expires)
    • 304协商缓存(Last-Modified 和 Etag)

5. 服务器接收到请求,并进行处理,最后把信息返回给客户端

  • HTTP响应报文(所有服务器返回给客户端的内容)
    • 响应起始行
    • 响应首部(响应头)
      • data存储的是服务器的时间
      • ...
    • 响应主体
    • 服务器返回的时候是:先把响应头信息返回,然后继续返回响应主体中的内容(需要的信息大部分都是基于响应主体返回的)

6.断开TCP链接通道(四次挥手)

  • 第一次挥手:由浏览器发起,发送给服务器,我已经把请求报文都给你了,你准备关闭吧
  • 第二次挥手:由服务器发起,告诉浏览器,我接收完成请求报文,我准备关闭,你也准备吧
  • 第三次挥手:由服务器发起,告诉浏览器,我响应报文发送完毕,你准备关闭吧
  • 第四次挥手:由浏览器发起,告诉服务器,我响应报文接收完毕,我准备关闭,你也准备吧

7.客户端渲染服务器返回来的结果

Connection:keep-Alive 服务器端设置

  • 保持TCP不中断(性能优化点:减少每一次请求还需要重新建立链接通道的时间)

浏览器的渲染及优化

  1. 在真实项目中,如果CSS样式代码不是很多的(再或者是移动端的项目),我们应该使用内嵌式,以此来减少HTTP资源的请求,提升页面渲染的速度
  2. 网络资源请求或HTTP请求的最大并发数
  3. 大部分浏览器都维持在七个左右(为了避免并发的上限,导致某些资源要延迟加载,页面渲染速度变慢,我们应该尽量可能减少HTTP的请求数量)
  4. 现代浏览器都有完善的代码扫描机制:如果遇到script需要同步加载和渲染代码,浏览器会在渲染JS的同时向下继续扫描代码,如果发现有一些异步的资源代码,此时就开始向服务器端加载请求了
  5. 在JS中还有可能操作元素的样式,所以哪怕都是异步请求资源的情况下,JS先加载回来,也要等到它之前发送的CSS加载并渲染完成后才会执行JS代码
页面加载的过程中遇到JSJS中会有操作DOM的代码):
【默认script src = 'xxx'】
主线程会从服务器获取到JS资源,并且把JS资源进行解析加载,加载完成后再继续渲染DOM结构
【如果设置了defer或者async】
都是让其变为异步获取资源(不会阻碍DOM的渲染)
//=> defer可以遵循原有的加载顺序,获取后按照顺序去依次渲染JS
//=> async无序的(谁先获取到先执行谁)
​
​
为啥要把link写在结构的上面,把script写在结构的下面?
//=> link放到顶部是为了更快加载回来的CSS
//=> script放到底部是为了获取DOM元素或者不阻碍DOM的渲染
​
​
DOMContenLoaded 事件:当DOM结构加载完成就会触发
//=> DOM树有了,并且JS也执行加载了,此时触发这个事件
load 事件:当所有资源都加载完成才会触发
//=> 包含了需要等待图片等资源也都加载完成触发JQ类库中:$(fucntion(){}) 或者 $(docunemt).ready(function(){})
//=> 当DOM结构加载完成才会执行函数中的代码
//=> 原理就是应用DOMContenLoaded事件完成的
//=> DOMContenLoaded在低版本浏览器中不兼容,不兼容使用onreadystatechange事件代替在这个事件中监听document.readyState值,只为complete代表DOM结构加载完成

浏览器中DOM的重绘与回流

  • 重绘(Repaint):元素样式的改变(但是宽高、大小、位置不变)
  • 回流:元素的大小或者位置发生了变化(当页面布局和几何信息发生变化的时候),触发了重新布局导致渲染树重新计算布局和渲染

注意:回流一定会触发重绘,而重绘不一定触发回流

避免DOM的回流

  1. 放弃传统操作dom的时代,基于Vue/react开始数据影响视图模式
  2. 分离读写操作(现代的浏览器都是有渲染队列的机制)
  3. 动画效果应用到position属性absolute或flxed的元素上(脱离文档流)
  4. CSS3硬件加速(GPU加速 :所有触发硬件加速的东西都不会引发回流和重绘)
    1. 例如:transform / opacity / filters ...这些属性会触发硬件加速,不会引发回流和重绘。可能会引发的坑:过多的使用会占用大量内存,性能消耗严重,有时候会导致字体模糊
  5. 牺牲平滑度换取速度
  6. 避免table布局和使用CSS的JavaScript表达式

注意:能用CSS3动画实现的就不用JS做动画这样可以减少回流

window.addEventListener('DOMContenLoaded', _=> {
    let navBox = document.getElementById('navBox');
    
    //=>渲染队列机制导致引发一次回流
    navBox.style.width = '100px';
    navBox.style.height ='100px';
    
    //触发两次
    navBox.style.width = '100px';
    console.log(navBox.clientWidth); //clientWidth获取盒子的宽度
    navBox.style.hdight ='100px';
​
    //=>10次回流
    for (let i = 0; i < 10; i++) {
        let span = docuement.createElement('span'); //创建一个元素对象
        navBox.appendChild(span);
    }
    //一次回流
    let frag = document.createDocumentFragment();
    for (let i = 0; i < 10; i++) {
        let span = docuement.createElement('span');
        frag.appendChild(span);
    }
    navBox.appendChild(frag);
    
    //可以基于ES6的模板字符串
    let str = ``;
    for (let i = 0; i < 10; i++) {
       str += `<span></span>`;
    }
    navBox.innerHTML = str;
})

前端性能优化总结

1. 减少HTTP请求次数和请求的大小

  • 文件合并压缩(代码偏少的情况下,尽可能使用内嵌式)
  • 雪碧图 CSS sprice 或 图片base64
  • 使用字体图标
  • 对于动态获取的图片,采用图片懒加载(数据也做异步分批加载,开始只请求加载第一屏的数据,滑动到第几屏在加载这一屏的数据和图片)
  • 骨架屏技术(首屏内容由服务器渲染、在或者开始展示展位结构,客户端再单独获取数据渲染)
  • 音视频取消预加载(播放的时候再去加载音视频文件,对于自动播放采取延迟播放的处理)
  • 在客户端和服务器端进行信息交互的时候,对于多项数据我们尽量可能基于JSON格式来进行传输(JSON格式的数据处理方便,资源偏小)
  • 开启服务器端的gzip压缩

2. 缓存机制

把一些请求回来的信息进行本地存储(缓存存储),在缓存有效期内,再次请求资源,直接从缓存中获取数据,而不是服务器上从新拉取

  • DNS预获取
  • 资源文件的强缓存和协商缓存(304)
  • 数据也可以做缓存(把从服务器获取的数据存储到本地:cookie / localstorage / redux / vuex 等,设定期限,在期限内,直接从本地获取数据即可)
  • 离线存储(一般很少用)manifest
  • CDN区域分布式服务器开发部署(费钱 效果非常好)

3. 代码上的优化

  • 减少DOM的重绘回流
  • 在JS中尽量减少闭包的使用(内存优化)
  • 在JS中避免“嵌套循环”和“死循环”
  • 尽可能使用事件委托
  • 尽可能减少CSS表达式的使用(expression)
  • CSS选择器解析规则是从右向左解析(基于less / sass开发的时候尽可能减少层级嵌套,目的让选择器的前缀短一点)【 a{ } 和 .box a { }】
  • 尽可能实现JS的封装(低耦合高内聚),减少页面代码的冗余代码
  • 在CSS导入的时候尽量减少使用@import导入式
  • 使用window.requestAnimationFrame(JS中的帧动画)代替传统的定时器动画(能用CSS3中的动画绝不用JS中的动画)
  • 减少递归的使用,避免死递归,避免由于递归导致的栈内存嵌套
  • 基于SCRIPT调取JS的时候,可以使用 defer 或者 async 来异步加载
  • 避免使用while语句
  • .....