01.浏览器的工作原理

前言

“呼呼,心中几分感慨,就是憋不出来话来,直接开搞把!”

​  咱前端大部分工作都离不开浏览器,对浏览器工作原理有一定的了解,即可以为性能优化时提供方向和理论依据,又能闭个环,何乐而不为呢?

PS:本文主要参考 浏览器工作原理探究 及度娘中各位大牛帖子写的小总结,有不对的地方,期待大家指点。

浏览器结构

  1. 用户界面:除了主窗口显示请求的页面外,地址栏、前进/后退按钮、书签菜单等各部分都属于用户界面;
  2. 浏览器引擎:在用户界面和渲染引擎之间传送指令;
  3. 渲染引擎(内核):负责显示请求的内容,如果请求内容是HTML,它负责解析HTML和CSS内容,并呈现,如果是压缩包等,则通过线程去下载;
  4. 网络:用于网络调用,如HTTP请求,接口与平台无关,为所有平台提供底层实现;
  5. 用户界面后端(UI后端):用于绘制基本窗口小部件,与平台无关的通用接口,在底层使用操作系统的用户界面方法;
  6. JavaScript解释器(JS引擎):用于解析和执行JavaScript代码;
  7. 数据存储:浏览器在硬盘上保存各种数据,如Cookie等,新的HTML5定义了网络数据库。

我们所俗称的浏览器内核其实由渲染引擎JS引擎组成,但是随着JS引擎越来越独立,现在提起的内核更倾向于只指——渲染引擎。

浏览器结构

常见内核及JS引擎特点

内核

  • firefox:gecko引擎;
  • IE:早期使用Trider引擎,现在改名为edge,使用edge引擎;
  • opera和chrome:13年后均开始时候Blink引擎;
  • UC:使用U3引擎;
  • QQ和微信:16年后开始使用Blink引擎;

V8引擎特点

​   chrome使用的是V8引擎,它的特点是直接将JavaScript代码直接编译成机器码运行,而传统引擎则是转变为字节码或中间表示再转成机器码,转换时间上就有了明显的区别。

“对引擎有兴趣小伙伴可浏览 某书:什么是JavaScript引擎 。”

进程和线程

"咳咳,这里不作长篇大论,就作概念知识铺垫,有兴趣小伙伴移步到 某乎:图解浏览器工作原理 了解。

​   电脑启动浏览器时,计算机或服务器就会创建一个进程,操作系统会为进程分配内存保存浏览器状态,浏览器则创建多个线程来辅助工作(关系如同一个车间多个工人)

Chrome采用多进程架构,其职责如下:(多进程通过IPC通信)

  • Browser Process:负责地址栏、书签栏、前后退按钮,及底层看不见操作,如网络请求;
    • UI thread : 控制浏览器上的按钮及输入框,如判断输入的是 URL 还是 query;
    • network thread: 处理网络请求,从网上获取数据,如执行 DNS 查询,建立连接并根据Content-Type和MIME Type sniffing判断内容格式:HTML传递给Renderer Process,其余传给下载管理器;
    • storage thread: 控制文件等的访问;
  • Renderer Process:负责tab页内网页呈现所有事情,即转换HTML/CSS/JS成用户可交互的 web 页面;
    • 主线程 Main thread
    • 工人线程 Worker thread:H5之前JS以单线程方式工作,H5引入工人线程实现多线程编程;
    • 光栅线程 Raster thread:早期将页面元素转化为显示器的像素的过程,现在则是合成线程实现;
    • 排版线程 Compositor thread:也叫合成器线程;将页面分成若干层分别进行光栅化,最后合并成一个页面的技术;
  • Plugin Process:负责控制一个网页用到的所有插件,如 flash
  • GPU Process:负责处理GPU相关任务。

导航流程

​   我们大多数人使用浏览器最多的场景就是在地址栏中输入关键字进行搜索或输入地址导航到某个网站,那么这个导航过程又是怎么实现的呢?

​   承接上一小节,我们了解到浏览器Tab外的工作都由 Browser Process 掌控,又细分为以下线程进行处理:

  • UI thread : 控制浏览器上的按钮及输入框;
  • network thread: 处理网络请求,从网上获取数据;
  • storage thread: 控制文件等的访问;

现在让我们来了解一下整个过程小细节吧:

  1. 处理输入:UI thread 需要判断用户输入的是 URL 还是 query;

  2. 开始导航:当用户点击回车键,UI thread通知network thread获取网页内容,并控制tab上的标签名展现,表示正在加载中;

  3. HTTP请求:network thread 会执行 DNS 查询,随后为请求建立 TLS 连接,如果接收到重定向如请求头301,network thread 会通知 UI thread 服务器要求重定向,之后,另外一个 URL 请求会被触发;

  4. 读取响应:当请求响应返回的时候,network thread 会依据 Content-Type 及 MIME Type sniffing 判断响应内容的格式:
    a.如果响应内容的格式是 HTML ,下一步将会把这些数据传递给 renderer process,如果是 zip 文件或者其它文件,会把相关数据传输给下载管理器;
    b.Safe Browsing检查也会在此时触发,如果域名或者请求内容匹配到已知的恶意站点,network thread 会展示一个警告页。此外 CORB 检测也会触发确保敏感数据不会被传递给渲染进程;

  5. 查找渲染进程:当上述所有检查完成,network thread 确信浏览器可以导航到请求网页,network thread 会通知 UI thread 数据已经准备好,UI thread 会查找到一个 renderer process 进行网页的渲染。

    PS:由于网络请求获取响应需要时间,这里其实还存在着一个加速方案。当 UI thread 发送 URL 请求给 network thread 时,浏览器其实已经知道了将要导航到那个站点。UI thread 会并行的预先查找和启动一个渲染进程,如果一切正常,当 network thread 接收到数据时,渲染进程已经准备就绪了,但是如果遇到重定向,准备好的渲染进程也许就不可用了,这时候就需要重启一个新的渲染进程。

  6. 确认导航:进过了上述过程,数据以及渲染进程都可用了, Browser Process 会给 renderer process 发送 IPC 消息来确认导航,一旦 Browser Process 收到 renderer process 的渲染确认消息,导航过程结束,页面加载过程开始。

​   此时,地址栏会更新,展示出新页面的网页信息。history tab 会更新,可通过返回键返回导航来的页面,为了让关闭 tab 或者窗口后便于恢复,这些信息会存放在硬盘中。

HTTP请求流程

​   前端负责向后端请求数据和展示⻚⾯,后端负责接收请求和做出响应发回给前端,两者之间的协作的桥梁是API,API不就是URL,URL则正是HTTP连接的⼀种具体的载体。

  1. url解析:从URL中抽出域名字段;

  2. DNS域名解析:查找浏览器缓存,浏览器会缓存30分钟内访问的DNS信息;如未找到检查系统缓存,检查hosts文件,它保存了本地主机访问过的网站域名和IP数据;未找到,接着会查找本地路由器缓存;下一步找到ISP DNS缓存,即ISP服务器DNS缓存,最后通过递归查询,从根域名服务器向顶级域名服务器发送查找,直至获取到对应IP地址;

  3. HTTP请求发送:根据解析出的IP地址+端⼝,浏览器(应⽤层)发起HTTP请求;

    请求报文又分为:请求头header(也可细分为请求⾏和请求头)、请求体body;

    请求头包含:
    请求的⽅法(get、post、put…)
    协议(http、https、ftp、sftp…)
    ⽬标url(具体的请求路径已经⽂件名)
    必要信息(缓存、cookie之类)

    请求体包含:

    ​请求的具体数据

  4. 传输层转发:报文到达传输层,分别由TCP/UDP传输协议提供传输服务,在传输过程中,会通过分片技术,将大片数据分割成一段段报文段进行传输;

  5. 网络层转发:传输到⽹络层,⽹络层根据IP协议匹配对应网络路由,将报文段分割成数据包进行传输,然后根据ARP寻址得到接收⽅的Mac地址;

  6. 数据链路层转发:传输到达数据链路层,数据包分割为数据帧,发送给接收方,达到这个阶段,数据已经传输到目标网络中主机(服务器);

  7. 资源寻找:通过从链路层获取的数据帧,再次层层组装传递到应⽤层,根据url中资源路径开始进行寻找并进行响应;

  8. 结束:发送⽅收到响应报⽂后,根据报文状态进行判断并解析。

到这里,HTTP请求便结束了,下面补充一些小零食

服务器监听

​​   接收方,也就是我们常说的服务器,首先会启动操作系统,系统就绪启动HTTP进程,守护进程可能是IIS,Nginx,开始定位服务器的WWW文件夹,并启动附属模块,如PHP,然后想操作系统申请TCPL连接绑定在80端口,开始监听请求。

TCP协议及三次握手

​   TCP: 传输控制协议,是⼀种⾯向连接的、可靠的、基于字节流的传输层通信协议。tcp是服务于⽹络通讯的⼀种协议,通讯双方需要建立一次TCP连接,即3次握手。

三次握手

  1. 客户端发送syn包(syn=j)到服务器,并进⼊SYN_SEND状态,等待服务器确认;
  2. 服务器收到syn包,必须确认客户的SYN(ack=j+1),同时⾃⼰也发送⼀个SYN包
    (syn=k),即SYN+ACK包,此时服务器进⼊SYN_RECV状态。
  3. 客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进⼊ESTABLISHED状态,完成三次握⼿,开始传输数据。

三次握手

四次挥手

  1. 由Client端发起中断连接请求,也就是发送FIN报文;
  2. Server端接到FIN报文后,这时可形象的描述:“我Client端没有数据要发给你了”,但是如果你还有数据没有发送完成,则不必急着关闭Socket,可以继续发送数据;

所以服务器会先发送ACK,答复:“告诉Client端,你的请求我收到了,但是我还没准备好,请继续你等我的消息”。这个时候Client端就进入FIN_WAIT状态,继续等待Server端的FIN报文;

当Server端确定数据已发送完成,则向Client端发送FIN报文,“告诉Client端,好了,我这边数据发完了,准备好关闭连接了”;

  1. Client端收到FIN报文后,"就知道可以关闭连接了,但是他还是不相信网络,怕Server端不知道要关闭,所以发送ACK后进入TIME_WAIT状态,如果Server端没有收到ACK则可以重传;
  2. Server端收到ACK后,“就知道可以断开连接了”。Client端等待了2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,我Client端也可以关闭连接了。Ok,TCP连接就这样关闭了!

在这里插入图片描述

UDP协议

​   UDP :⽤户数据报协议,相⽐于TCP的⾯向连接需要反复确认的步骤,UDP是⾮⾯向连接的,不可靠的传输层写协议,使⽤udp协议经常通信并不需要建⽴连接,它只是负责把数据尽可能快的发送出去,简单粗暴,并且不可靠。⽽在接收端,UDP把每个消息断放⼊队列中,接收端程序从队列中读取数据。
​​   虽然UDP不可靠,但是它的传输速度快,效率⾼,在⼀些对数据准确性要求不⾼的场景,UDP就变得很有⽤了,⽐如qq语⾳、qq视频。

HTTP状态码

  • 200 开头的就好办,表示请求成功,直接进入渲染流程;
  • 300 开头的就要去相应头里面找 location 域,根据这个 location 的指引,进行跳转,这里跳转需要开启一个跳转计数器,是为了避免两个或者多个页面之间形成的循环的跳转,当跳转次数过多之后,浏览器会报错,同时停止;
  • 400 开头是请求的资源(网页等)不存在;
  • 500 开头是内部服务器错误;

渲染流程

​从资源的下载到最终的页面展现,渲染流程可简单地理解成一个线性串联的过程,有如下基本流程:

  1. 处理 HTML 并构建 DOM Tree。
  2. 处理 CSS 并构建 CSSOM Tree。
  3. 将 DOM Tree 和 CSSOM Tree 合并成 Render Object Tree。
  4. 根据 Render Object Tree 计算节点的几何信息并以此进行布局。
  5. 绘制页面需要先构建 Render Layer Tree 以便用正确的顺序展示页面,这棵树的生成与 Render Object Tree 的构建同步进行。然后还要构建 Graphics Layer Tree 来避免不必要的绘制和使用硬件加速渲染,最终才能在屏幕上展示页面。

在这里插入图片描述

PS:为了达到更好的用户体验,渲染引擎力求尽快的将内容显示到屏幕上。它不会等到整个HTML文档解析完毕才开始构建渲染树和布局,而是在不断接收和处理内容的同时,将部分内容解析并显示,即逐步出现画面。

webKit渲染流程

在这里插入图片描述

小知识:框架树等同渲染树,布局等同重排,只是不同浏览器术语不同。

HTML解析——DOM树构建

解析器类型:
有两种常见的基本类型解析器:自上而下解析器和自下而上解析器:
自上而下解析器:从语法的高层结构出发,尝试从中找到匹配的结构;
自下而上的解析器:从低层规则出发,将输入内容逐步转化为语法规则,直至满足高层规则。
需要注意的是,HTML无法用常规的自上而下或自下而上解析器解析,原由:

  1. 语言的宽容本质。
  2. 浏览器历来对一些常见的无效 HTML 用法采取包容态度。
  3. 解析过程需要不断地反复。源内容在解析过程中通常不会改变,但是在 HTML 中,脚本标记如果包含 document.write,就会添加额外的标记,这样解析过程实际上就更改了输入内容。

由于不能使用常规的解析技术,浏览器就创建了自定义的解析器来解析 HTML,称为HTML解析器。

​ HTML解析是DOM Tree构建的过程,主要分为2个过程:

  1. 编码:将HTML原始字节数据转换为文件制定编码字符串;

  2. HTML算法解析:细分为2个过程:

    a. 标记化(词法解析):对输入字符串进行扫描,根据W3C构词规则识别单词和符号,解析成我们易理解的词汇(学名Token)过程;

    b. 树构建(语法分析):对Tokens应用HTML语法规则,进行配对标记、确立节点关系和绑定属性等操作,从而构建DOM Tree过程。

HTM解析

HTML算法

​   接下来,了解一下HTML解析算法,即标记化树构建两个过程:

词法解析/标记化

​   将HTML结构解析成多个标记,如起始标记,结束标记,属性名称,属性值等,用于对应一个HTML元素。标记生成器通过识别标记,传递给树构造器,然后接收下一个标记进行识别,返回直至结束,标记化算法是通过状态机实现的,而状态机模型是由W3C中已经定义好的;

例子:<a href="www.w3c.org">W3C</a>

在这里插入图片描述

  • 开始标记:
  • Data state:碰到 <,进入 Tag open state
  • Tag open state:碰到 a,进入 Tag name state 状态
  • Tag name state:碰到 空格,进入 Before attribute name state
  • Before attribute name state:碰到 h,进入 Attribute name state
  • Attribute name state:碰到 =,进入 Before attribute value state
  • Before attribute value state:碰到 ",进入 Attribute value (double-quoted) state
  • Attribute value (double-quoted) state:碰到 w,保持当前状态
  • Attribute value (double-quoted) state:碰到 ",进入 After attribute value (quoted) state
  • After attribute value (quoted) state:碰到 >,进入 Data state,完成解析
  • 内容标记:W3C
  • Data state:碰到 W,保持当前状态,提取内容
  • Data state:碰到 <,进入 Tag open state,完成解析
  • 结束标记:
  • Tag open state:碰到 /,进入 End tag open state
  • End tag open state:碰到 a,进入 Tag name state
  • Tag name state:碰到 >,进入 Data state,完成解析
语法解析/树构建

​   标记生成器发送的每个Tokens都由树构建器进行迭代,根据当前Tokens的类型创建对应的元素,添加到DOM Tree中,此栈的还有一个目的:实现浏览器的容错机制,纠正前台错误。更多标记处理也是由状态机实现的。

HTML解析完毕后,浏览器会将文档状态标注为交互状态,开始解析处于deferred模式的脚本,文档状态解析完后设置为完成状态,下一个加载随之触发

CSS解析——CSSOM树构建

加载

​   在构建DOM Tree的过程中,如果遇上Link标记,浏览器就会立即发送请求去获取样式文件,当然也可以通过内嵌和行内方式减少请求,但是失去了模块化和可维护性,性价比就变低了,一般除了极致优化首页加载,都不推荐这么做。

阻塞

​   首先,CSS的加载和解析并不会阻塞DOM Tree的构建,因为DOM Tree和CSSOM Tree是两棵相互独立的树结构,但是这个过程会阻塞页面渲染,也就是说:在没有处理完CSS之前,文档是不会在页面上显示出来的,这个策略在于不让页面重复渲染;

假设:DOM Tree构建完毕就直接渲染,那么此时页面显示时一个原始的样式,当CSSOM Tree树构建完毕,页面又突然渲染成另一个模样,即影响了用户体验,又造成没必要的开销;

其次,link标记阻塞JavaScript运行,在这种情况下,DOM Tree是不会继续构建,因为JavaScript也会阻塞DOM Tree构建,造成长时间的白屏

  • 为什么需要阻塞JavaScript的运行呢?

    因为JavaScript可以操作DOM和CSSOM,如果link标记不阻塞JavaScript的运行,这时,JavaScript操作CSSOM时,就会发生冲突!

解析

​   CSS解析的步骤和HTML解析是相似的。

词法解析

​   CSS会被拆分成如下一些标记:
在这里插入图片描述

语法分析

​   每个CSS文件嵌入样式都会被解析成CSS StyleShee对象,这个对象由一些了的Rule(规则)组成,每一条Rule都包含Selectors(选择器)和若干Declearation(声明),Declearation又由Property(属性)和Value(值)组成,简单图解:

在这里插入图片描述

​   另外,浏览器默认样式表,用户样式表也会有对象的CSS StyleShee对象,因为它们也是单独的CSS文件,至于内联样式,在构建DOM Tree时会直接解析成Declearation集合

在这里插入图片描述

1. 内联样式和CSS StyleShee的区别

​   所有的 CSS StyleShee 都挂载在 document 节点上,可以通过浏览器document.styleSheets 获取到这个集合。最后生成 ComputedStyle 对象,可以通过 window.getComputedStyle 方法查看所有声明。而内联样式可以通过节点 style 属性查看。

例子:

 ...//内嵌及外部样式表
 <style>
    body .div1 {
      line-height: 1em;
    }
  </style>
  <link rel="stylesheet" href="./style.css">
  <style>
    .div1 {
      background-color: #f0f;
      height: 20px;
    }
  </style>
  <title>Document</title>
</head>
<body>
 // 内联
  <div class="div1" style="background-color: #f00;font-size: 20px;">test</div>
</body>

在这里插入图片描述

2. 需要属性合并吗?

​   在解析 Declearation 时遇到属性合并,会把单条声明转变成对应的多条声明,如:margin: 20px 就会被转变成四条声明;

​   这说明 CSS 虽然提倡属性合并,但是最终还是会进行拆分的;所以属性合并的作用应该在于减少 CSS 的代码量。

计算

​   计算的来源在于:一个节点可能有多个selector命中它,这就需要把所有匹配规则组合起来,再设置最后的样式。

  • 准备工作

    为了便于计算,在生成CSS SytleSheet对象后,会把对象最右边的Selector类型相同的Rules存放到对应的Hash Map中,比如选择器类型是ID的Rules就会放到ID Rule Map中;

    从右向左匹配规则

    过程:这里提到 Hash Map 存放的是最右边 Selector 类型的 Rule,所以在查找符合的 Rule 最开始,检验的是当前 Rule 最右边的 Selector;如果这一步通过,下面就要判断当前的 Selector 是不是最左边的 Selector;如果是,匹配成功,放入结果集合;否则,说明左边还有 Selector,递归检查左边的 Selector 是否匹配,如果不匹配,继续检查下一个 Rule。

    为什么是从右向左匹配呢?

    首先,我们了解一下正向匹配的流程:以div p .yellow 来举例,先查找所有 div 节点,再向下查找后代是否是 p 节点,如果是,再向下查找是否存在包含 class=“yellow” 的节点,如果存在则匹配;但是不存在呢?就浪费一次查询,如果一个页面有上千个 div 节点,而只有一个节点符合 Rule,就会造成大量无效查询,并且如果大多数无效查询都在最后发现,那损失的性能就实在太大了;

    :从右向左匹配的好处,如果一个节点想要找到匹配的 Rule,会先查询最右边 Selector 是当前节点的 Rule,再向左依次检验 Selector;在这种匹配规则下,开始就能避免大多无效的查询,当然性能就更好,速度更快了。

  • 选择器命中

    一个节点想获取到所有匹配的Rule,需要依次判断Hash Map中的Selector类型(ID,class,tagName等)是否匹配当前节点,如果匹配就会筛选当前类型所有的Rule放到结果集合中,注意,通配符是最后筛选的;

  • 优先级判断

    Rule放入结果集合的同时会计算这条 Rule 的优先级,因为解析Rule顺序是从右向左进行的,所以计算也会按照这个顺序进行权重相加,当节点匹配的Rule放入到结果集合之后,会根据优先级从小到大排序,如相同优先级,则比较位置。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aQ3961Fy-1593224796695)(C:\Users\Administrator\Desktop\沉淀\选择器权重.png)]

到这里,CSSOM Tree就正式构建完成

构建渲染树

​   在DOM Tree和CSSOM Tree构建完毕后,才会开始生成Render Object Tree。生成渲染树时,会计算每一个渲染对象的可视化属性。

​   即将每个DOM节点的一个’attach’方法,在节点插入DOM树时会调用节点的attach方法,计算该节点的样式属性生成渲染对象,一个个Render Object形成一棵完整的Render Object Tree。
在这里插入图片描述

布局/重排

​   Render Object 在添加到树之后,还需要重新计算位置和大小。

为什么要重新计算呢?

因为像margin:0 auto;这样的声明时不能直接使用的,需要转换为实际的大小,才能通过绘图引擎绘制节点;这也是DOM Tree 和CSSOM Tree需要组合成Render Object Tree的原因之一。

​   布局时用Root Render Object 开始递归的,每个Render Object都有一个对自身进行布局的方法:'layout’或者’reflow’方法,根据自己需要调用布局方法。如:正常流文字排版,绝对定位,浮动元素排版,flex排版等。

为什么要递归呢?

因为有些布局信息需要子节点先计算,之后才能通过子节点的布局信息计算出父节点的位置和大小。而对于子节点依靠父节点高度这类型,则是在计算子节点之前,先计算自身布局信息,再传递给子节点,子节点根据这些信息计算好后再告诉父节点是否需再重新计算。

构建呈现层树

​   Render Layer 是在 Render Object 创建的同时生成的,具有相同坐标空间的 Render Object 属于同一个 Render Layer。这棵树主要用来实现BFC层叠上下文,以保证用正确的顺序合成页面。

也就是说,这个阶段是为了获取展示页面时的正确顺序。

构建图形层树

软件渲染

​   软件渲染是浏览器最早采用的渲染方式。在这种方式中,渲染是从后向前(递归)绘制 Render Layer 的;在绘制一个 Render Layer 的过程中,它的 Render Objects 不断向一个共享的 Graphics Context 发送绘制请求来将自己绘制到一张共享的位图。

硬件渲染

​   有些特殊的 Render Layer 会绘制到自己的后端存储,而不是整个网页共享的位图中,这些 Layer 被称为 Composited Layer。最后,当所有的 Composited Layer 都绘制完成之后,会将它们合成到一张最终的位图中,这一过程被称为 Compositing;这意味着如果网页某个 Render Layer 成为 Composited Layer,那整个网页只能通过合成来渲染。

​   除此之外,Compositing 还包括 transform、scale、opacity 等操作,所以这就是硬件加速性能好的原因,上面的动画操作不需要重绘,只需要重新合成就好。

为什么需要Composited Layer?

  1. 避免不必要的重绘。例如网页中有两个 Layer a 和 b,如果 a Layer 的元素发生改变,b Layer 没有发生改变;那只需要重新绘制 a Layer,然后再与 b Layer 进行 Compositing,就可以得到整个网页。
  2. 利用硬件加速高效实现某些 UI 特性。例如滚动、3D 变换、透明度或者滤镜效果,可以通过 GPU(硬件渲染)高效实现。

绘制

将位图绘制到屏幕上,变成肉眼可见的图像的过程

​   一般来说,浏览器并不需要用代码来处理这个过程,浏览器只需要把最终要显示的位图交给操作系统即可。

PS:回顾,从HTTP请求回来的数据通过HTML解析器和CSS解析器,分别解析成DOM树和CSSOM树,然后两者结合生成Render树,渲染树通过调用Layout方法进行布局,这个过程同时也会生成一个呈现树,然后通过合成将渲染树和呈现树变成图层树,即位图,再将位图给操作系统进行绘制到屏幕上。

常见问题

重绘和回流

​   重绘和回流是在页面渲染过程中非常重要的两个概念。页面生成以后,脚本操作、样式表变更,以及用户操作都可能触发重绘和回流。

重绘

​   重绘是指当与视觉相关的样式属性值被更新时会触发绘制过程,在绘制过程中要重新计算元素的视觉信息,使元素呈现新的外观。

​   重绘repaint只发生在渲染层 render layer上。所以,如果要改变元素的视觉属性,最好让该元素成为一个独立的渲染层,如:

  • display: none/block,会引起回流,从而引起重绘,性能较差;
  • visibility: visibile/hidden,只引起重绘,性能较好
回流/重排

​   回流reflow是firefox里的术语,在chrome中称为重排relayout。

​   回流是指窗口尺寸被修改、发生滚动操作,或者元素位置相关属性被更新时会触发布局过程,在布局过程中要计算所有元素的位置信息。由于HTML使用的是流式布局,如果页面中的一个元素的尺寸发生了变化,则其后续的元素位置都要跟着发生变化,也就是重新进行流式布局的过程,被称之为回流;

​回流发生在Render树上,常说的脱离文档流就是脱离Render Tree,常见触发回流的操作:

  1. DOM元素几何属性变化;
  2. DOM树的结构变化;
  3. 获取或修改一些样式:offsetTop\offsetLeft\offsetWidth\offsetHeight…;
  4. 调整浏览器窗口大小.。

​   回流必将重绘,因为对一个元素的回流,可能会影响父元素,所以在性能优化上来说,尽量只触发小规模的重绘,尽量避免回流。

常见性能优化

下面列举一些减少回流次数的方法:

1、不要一条一条地修改DOM样式,而是修改className或者修改style.cssText;

2、在内存中多次操作节点,完成后再添加到文档中去;

3、对于一个元素进行复杂的操作时,可以先隐藏它,操作完成后再显示;

4、在需要经常获取那些引起浏览器回流的属性值时,要缓存到变量中;

5、不要使用table布局,因为一个小改动可能会造成整个table重新布局,而且table渲染通常要3倍于同等元素时间。

JavaScript脚本解析过程

正常加载JS脚本

主要通过script元素完成。其正常流程如下

  1. 浏览器的呈现引擎持有渲染的控制权,它正常解析HTML页面;
  2. 解析遇到script标签,呈现引擎移交控制权给Javascript引擎(例如chrome的V8);
  3. 如果script标签引用了外部脚本那就先下载再执行,否则直接执行代码;
  4. JavaScript引擎执行完毕移交控制权给呈现引擎,呈现引擎继续解析;

加载外部脚本时,浏览器会暂停页面渲染,等待脚本下载并执行完成后,再继续渲染。原因是 JavaScript 代码可以修改 DOM,所以必须把控制权让给它,否则会导致复杂的线程竞赛的问题。

defer属性

浏览器解析到包含defer属性的script元素时,其运行流程如下:

  1. 浏览器的呈现引擎持有渲染的控制权,它正常解析HTML页面;
  2. 解析遇到包含defer属性的script标签,继续解析HTML,同时并行下载外链脚本;
  3. 解析完成,文档处于交互状态时开始解析处于deferred模式的脚本;
  4. 脚本解析完毕后,将文档状态设置为完成,DOMContentLoaded事件随之触发;

使用defer属性时需要注意的点:

  1. defer属性下载的脚本文件在DOMContentLoaded事件触发前执行(即刚刚读取完html标签);
  2. defer属性可以保证执行顺序就是它们在页面上出现的顺序;
  3. 对于内置而不是加载外部脚本的script标签,以及动态生成的script标签,defer属性不起作用;
  4. 使用defer加载的外部脚本不应该使用document.write方法。
async属性

浏览器解析到包含async属性的script元素时,其运行流程如下::

  1. 浏览器的呈现引擎持有渲染的控制权,它正常解析HTML页面;
  2. 解析遇到包含async属性的script标签,继续解析HTML,让另一进程同时并行下载外链脚本;
  3. 脚本下载完成,浏览器暂停解析HTML,开始执行下载的脚本脚本执行完毕,浏览器恢复解析HTML

使用async属性时需要注意的点:

  1. async属性可以保证脚本下载的同时,浏览器继续渲染;
  2. async属性无法保证脚本的执行顺序,哪个先下载结束就先执行哪一个;
  3. 包含async属性的脚本不应该使用document.write方法;
  4. 如果同时使用async和defer属性,后者不起作用,浏览器行为由async属性决定。
脚本的动态加载

​   script元素还可以动态生成,生成后再插入页面,从而实现脚本的动态加载。动态生成的script标签不会阻塞页面渲染,也就不会造成浏览器假死。
​   但是问题在于,这种方法无法保证脚本的执行顺序,哪个脚本文件先下载完成,就先执行哪个。如果想避免这个问题,可以设置async属性为false。还可以监听脚本的onload事件来为脚本指定回调。

CSS阻塞JS加载

​   因为JS脚本可能会引用DOM的样式做计算,所以为了保证脚本计算的正确性:Firefox浏览器会等到脚本前面的所有样式表,都下载并解析完,再执行脚本;Webkit则是一旦发现脚本引用了样式,就会暂停执行脚本,等到样式表下载并解析完,再恢复执行。

​   此外,对于来自同一个域名的资源,比如脚本文件、样式表文件、图片文件等,浏览器一般有限制,同时最多下载6~20个资源,即最多同时打开的 TCP 连接有限制,这是为了防止对服务器造成太大压力。如果是来自不同域名的资源,就没有这个限制。所以,通常把静态文件放在不同的域名之下,以加快下载速度。

浏览器预解析/预下载

​   WebKit和Firefox都进行了这项优化。在执行脚本时,其他线程会解析文档的其余部分,找出并加载需要通过网络加载的其他资源。通过这种方式,资源可以在并行连接上加载,从而提高总体速度。

​   请注意,预解析器不会修改DOM树,而是将这项工作交由主解析器处理;预解析器只会解析外部资源(例如外部脚本、样式表和图片)的引用。

浏览器缓存

​   工作中,前端代码打包后生成的静态资源要发布到静态服务器上,这时就需要对这些静态资源做一些运维配置,其中,gzip和设置缓存是必不可少的,这两项直接影响到网站性能和用户体验。

缓存的优点:

  • 减少了不必要的数据传输,节省带宽
  • 减少服务器的负担,提升网站性能
  • 加快了客户端加载网页的速度
  • 用户体验友好

缺点:

  • 资源如果有更改但是客户端不及时更新会造成用户获取信息滞后,如果老版本有bug的话,情况会更加糟糕。

缓存类别:

  1. Page Cache:是将浏览器页面状态临时保持在缓存中,以加速页面返回等操作;
  2. Memonry Cache:浏览器内部缓存机制,对相同RUL的资源直接从缓存获取,不再重新下载;
  3. Disk Cache::资源加载缓存和服务器进行交互,服务器端可以通过HTTP头设置要不要缓存。

Memory Cache

​   内存缓存,其主要作用为缓存页面使用各种派生资源。在使用浏览器浏览网页时,尤其是浏览一个大型网站的不同页面时,经常会遇到网页中包含相同资源的情况,应用Memory Cache可以显著提高浏览器的用户体验,减少无谓的内存、时间以及网络带宽开销

Page Cache:

​   即页面缓存,用来缓存用户访问过的网页DOM树、Render树等数据。设计页面缓存的意图在于提供流畅的页面前进、后退浏览体验。几乎所有的现代浏览器都支持页面缓存功能。

​   如果浏览器没有页面缓存,用户点击链接访问新页面时,原页面的各种派生资源、JavaScript对象、DOM树节点等占据的内存统统被回收,此后当用户点击后退按钮以浏览原页面时,浏览器必须先要重新从网络下载相关资源,然后进行解码、解析、布局、渲染一系列操作,最后才能为用户呈现出页面,这无疑增加了用户的等待时间,影响了用户的使用体验。

​   所有的派生资源加载时都会与Memory Cache关联,如果Memory Cache中有资源的备份且条件合适,则可以直接从Memory Cache中加载。而Page Cache只会在用户点击前进或后退按钮时才会被查询,如果页面符合缓存条件并被缓存了,则直接从Page Cache中加载。即使某个需要被加载的页面在Page Cache中有备份,但若触发加载的原因是用户在地址栏输入url或点击链接,则页面仍然是通过网络加载。也就是说Page Cache并不是主资源的通用缓存

早期将webkit的资源分类主要分为两大类:主资源和派生资源,

一类是主资源,比如HTML页面,或者下载项,

一类是派生资源,比如HTML页面中内嵌的图片或者JS脚本、样式表链接,分别对应代码中两个类:
MainResourceLoader和SubresourceLoader。
自从有了PageCache后,主资源也可以缓存,区分主资源和派生资源则是通过CachedResource类里面Type类型:

enum Type {

MainResource, //主资源

ImageResource,

CSSStyleSheet,

Script,

FontResource,

RawResource };

Disk Cache

​   磁盘缓存。现代的浏览器基本都有磁盘缓存机制,为了提升用户的使用体验,浏览器将下载的资源保存到本地磁盘,当浏览器下次请求相同的资源时,可以省去网络下载资源的时间,直接从本地磁盘中取出资源即可;

​   磁盘缓存也是我们常问到的Web缓存,分为强缓存和协商缓存,它们的区别在于强缓存不发请求到服务器,协商缓存会发请求到服务器。

强缓存

​   强缓存,强即强制;当浏览器去请求某个文件时,服务器就在respone header里对该文件做缓存配置:如缓存事件、缓存类型等,具体表现:

  • respone header 的cache-control
    • max-age表示缓存的时间,单位秒;
    • public表示可以被浏览器和代理服务器缓存,代理服务器一般可用nginx来做;
    • private表示只让客户端可以缓存该资源;代理服务器不缓存;
    • immutable表示该资源永远不变,即让用户在刷新页面时不要去请求服务器!也就是强缓存,当用户每次打开这个页面,浏览器会判断缓存是否过期,未过则读取;

例子:

  1. cache-control: max-age=xxxx,public
    客户端和代理服务器都可以缓存该资源;
    客户端在xxx秒的有效期内,如果有请求该资源的需求的话就直接读取缓存,statu code:200 ,如果用户做了刷新操作,就向服务器发起http请求

  2. cache-control: max-age=xxxx,private
    只让客户端可以缓存该资源;代理服务器不缓存
    客户端在xxx秒内直接读取缓存,statu code:200

  3. cache-control: max-age=xxxx,immutable
    客户端在xxx秒的有效期内,如果有请求该资源的需求的话就直接读取缓存,statu code:200 ,即使用户做了刷新操作,也不向服务器发起http请求

  4. cache-control: no-cache
    跳过设置强缓存,但是不妨碍设置协商缓存;一般如果你做了强缓存,只有在强缓存失效了才走协商缓存的,设置了no-cache就不会走强缓存了,每次请求都回询问服务端。

  5. cache-control: no-store
    不缓存,这个会让客户端、服务器都不缓存,也就没有所谓的强缓存、协商缓存了。

协商缓存

​   强缓存就是给资源设置个过期时间,客户端每次请求资源时都会看是否过期;只有在过期才会去询问服务器。所以,强缓存就是为了给客户端自给自足用的;

​   而当客户端请求该资源时发现其过期了,这是就会去请求服务器了,而这时候去请求服务器的这过程就可以设置协商缓存。这时候,协商缓存就是需要客户端和服务器两端进行交互的

协商缓存配置
// response header里面的设置:
etag: '5c20abbd-e2e8'
last-modified: Mon, 24 Dec 2018 09:49:49 GMT
  • etag:每个文件有一个,改动文件了就变了,就是个文件hash,每个文件唯一,就像用webpack打包的时候,每个资源都会有这个东西,如: app.js打包后变为 app.c20abbde.js,加个唯一hash,也是为了解决缓存问题;

  • last-modified:文件的修改时间,精确到秒;

交互过程
  1. 每次请求返回来 response header 中的 etag和 last-modified,在下次请求时在 request header 就把这两个带上;
  2. 服务端把你带过来的标识进行对比,然后判断资源是否更改了,如果更改就直接返回新的资源,和更新对应的response header的标识etag、last-modified;
  3. 如果资源没有变,那就不变etag、last-modified,这时候对客户端来说,每次请求都是要进行协商缓存。

资源未过期:

发请求–>看资源是否过期–>过期–>请求服务器–>服务器对比资源是否真的过期–>没过期–>返回304状态码–>客户端用缓存的老资源。

资源过期

发请求–>看资源是否过期–>过期–>请求服务器–>服务器对比资源是否真的过期–>过期–>返回200状态码–>客户端如第一次接收该资源一样,记下它的cache-control中的max-age、etag、last-modified等。

​ 需要注意的是,response header中的etag、last-modified在客户端重新向服务端发起请求时,会在request header中换个key名:

// response header
etag: '5c20abbd-e2e8'
last-modified: Mon, 24 Dec 2018 09:49:49 GMT

// request header 变为
if-none-matched: '5c20abbd-e2e8'
if-modified-since: Mon, 24 Dec 2018 09:49:49 GMT
为什么要有etag?

你可能会觉得使用last-modified已经足以让浏览器知道本地的缓存副本是否足够新,为什么还需要etag呢?

etag的出现是由HTTP1.1新增的,也就是说,它是为了解决之前只有If-Modified的缺点:

  1. 一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新get;

  2. 某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),if-modified-since能检查到的粒度是秒级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒);

  3. 某些服务器不能精确的得到文件的最后修改时间。

设置强缓存和协商缓存

  1. nodejs配置
res.setHeader('max-age': '3600 public')
res.setHeader(etag: '5c20abbd-e2e8')
res.setHeader('last-modified': Mon, 24 Dec 2018 09:49:49 GMT)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值