浏览器的渲染及web性能

214 阅读20分钟

web应用的生命周期

生命周期的各个阶段发生在用户动作、浏览器动作、服务器动作之中。

  1.用户从浏览器地址栏输入一串url或单击一个链接开始。
  2.浏览器生成请求并发送至服务器。
  3.服务器执行某个动作或者获取某个资源,并将响应发送给客户端。
  4.处理HTML、css、javascript并构建结果页面。
  5.监控事件队列每次处理一个事件。
  6.与页面元素进行交互。
  7.关闭web页面,web应用生命周期结束。

页面运行时的生命周期

由于服务器的响应一般由Html、css、javascript组成,浏览器拿到服务器的响应后开始构建结果页面。由此进入页面运行的生命周期。

页面构建包含以下两个过程
1.解析HTML代码并构建文档对象模型(DOM)。 
2.执行javascript代码。

由于多数浏览器使用单一进程来处理用户界面UI的刷新和javascript脚本执行,所以同一时刻只能做一件事情。

对于外部的JavaScript文件、css文件、图片,是按照它们在文档中出现的顺序逐个下载的,每个文件必须等到前一个文件下载并执行完成才会开始下载。在下载的过程中浏览器会停止处理页面。


一般来说,JavaScript代码能够在任何程度上修改DOM结构:它能创建新的节点或移除现有DOM节点(我们使用JavaScript代码来动态地修改DOM以便给Web应用带来动态行为。)。当浏览器遇到<script>标签时,当前HTML页面无从获知javascript是否会操作DOM。因此这时浏览器会停止处理页面,先执行javascript代码。等javascript的最后一行代码执行完,再继续解析和渲染页面。同样的情况也发生在利用<script>标签的src属性加载外部JavaScript的过程。浏览器必须花时间下载外部的JavaScript文件中的代码,然后解析并执行。在这个过程中浏览器会停止处理页面。

之前的浏览器只能按javascript在页面中出现的顺序逐个下载执行与此同时阻塞其他文件的下载,现在的浏览器都允许并行下载JavaScript文件。<script>标签在下载外部资源时不会阻塞其他<script>标签。但是JavaScript下载过程仍然会阻塞其他资源的下载,比如图片。尽管JavaScript脚本下载过程不会互相影响,但是页面必须等待所有JavaScript下载并执行完成才能继续。

其中1会在浏览器解析HTML节点的过程中执行,2会在解析HTML节点时遇到script脚本节点时执行。所以这两个过程会交替执行。

  • 页面构建的详细过程

    1.构建文档对象模型 DOM Tree
     根据获得到的HTML文档通过一系列的转换解析成HTML标签,并构建成DOM Tree。DOM Tree的构建过程是一个深度遍历的过程,当前节点的所有子节点构建好后才会去构建当前节点的下一个兄弟节点。
    2.构建CSS对象模型 CSSOM
      在构建文档对象模型的过程中,若遇到link标签,则浏览器会发出一个获取该资源的请求,最终获得包含有各种 CSS 样式的样式文件。与 HTML 文件一样,浏览器会将该文件通过一系列的转换和解析构建出对象模型,即 CSSOM。
    3.构建渲染树 Render Tree
     DOM Tree 描述的是文档内容,而CSSOM描述的是应用于文档的样式规则,二者是独立的对象。浏览器会把 DOM Tree 和 CSSOM组合起来构建渲染树,即 Render Tree。
    4.布局
     构建完渲染树后,浏览器便会从渲染树的根节点开始遍历,计算页面上每个对象的几何信息,这一个过程称为布局。布局输出的是一个个盒子模型,它精确的计算出每个元素在视口中的准确位置及尺寸大小。
    5.绘制
     最后一步是绘制,即将渲染树中的每个节点绘制成实际的像素点。 
    

    由于页面上的非可见元素并不会形成render树中的节点,因此render Tree中的节点并不与DOM Tree中的节点一一对应。

    juejin.cn/post/684490… www.html5rocks.com/zh/tutorial…

web应用生命周期的事件处理部分

客户端web应用是一种GUI应用,也就是说它会对用户执行的鼠标移动、单击、键盘按压等行为做出响应。因此,在页面构建阶段执行的JavaScript代码除了会影响全局的应用状态和修改DOM外,还会注册事件监听器,这些事件监听器会在事件发生时由浏览器调度执行,从而也就保证了应用与用户的交互能力。

  • 单线程执行模型

    浏览器执行环境的核心思想是基于:同一时刻只能执行一个代码片 段,即所谓的单线程执行模型
    
  • 事件队列

    因为浏览器是单线程的为了追踪已经发生但尚未处理的事件,浏览器使用了事件队列。
    放置事件的队列是在页面构建阶段和事件处理以外的。它对于决定事件何时发生并将其推入事件队列很重要。
    事件队列不会参与事件处理线程。
    
  • 事件是异步的

    事件的类型主要由以下几种:
    1.浏览器事件,例如,当页面加载完成后或无法加载时。
    2.网络事件,例如来自服务器的响应(ajax事件或者服务事件)
    3.用户事件,例如鼠标单击,鼠标移动和键盘事件。
    4.计时器事件。
    
  • 注册事件处理器

    事件处理器是当某个特定事件发生时我们希望处理的函数。为了事件能被处理我们必须告诉浏览器我们要处理哪个事件,这个过程就叫做注册事件处理器。
    所有的事件都以其出现的顺序储存在事件队列中。
    
  • 事件处理

    事件处理背后的思想逻辑:当事件发生时,浏览器调用相应的事件处理器。由于浏览器是单线程的执行模型,每次只能处理一个事件,其后的任何事件都只能等待当前事件处理器完全结束执行之后才能处理。
    事件的处理顺序是以事件的生成顺序。
    
  • 事件循环

     在事件处理阶段:
     1.浏览器检查宏任务队列列头,如果浏览器在队列列头中检测到了任务,则从宏任务队列中取出该任务,并执行该任务
     2.当位于宏任务队列列头的任务执行完毕后,浏览器会移动去处理微任务队列,如果微任务队列中有任务,则取出该任务,并依次执行余下任务,完成一个后执行下一个微任务,直到微任务队列为空
     3.当微任务处理完并清空时,浏览器会检查是否需要更新UI渲染,如果是则会重新渲染UI视图此时该轮循环结束,浏览器会进入下一轮循环
     4.浏览器会继续宏任务队列列头是否存在任务,如果存在,重复上述过程,如果不存在,则继续检查,以等待处理新到来的宏任务并一直循环此过程,直到用户关闭该浏览器页面这就是事件循环
     
     在事件循环的过程中,如果一个事件正在执行,余下的事件在事件队列中耐心等待,直到轮到它们被处理
     
    
  • 事件循环中包含两个任务队列

    事件循环的实现至少应该含有一个用于宏任务的队列和至少一个用于微任务的队列。
    
    • 宏任务

      宏任务主要包括:
      1.创建主文档对象,解析HTML2.执行主线JavaScript代码。
      3.更改当前URL,如网页加载和输入。
      4.事件,如用户点击事件,键盘按压事件,网络事件和定时器事件。
      
      从浏览器的角度来看,宏任务代表一个离散的独立的工作单元,当任务运行完,浏览器可以进行其他调度,如渲染页面的UI或执行垃圾回收。
      
    • 微任务

      微任务是更小的任务,必须在浏览器任务重新渲染页面的UI前执行
      微任务主要包括:
      1.promise回调函数
      2.DOM发生变化
      
      微任务能够让我们在浏览器重新渲染UI之前执行指定的行为避免不必要的重绘,重绘会导致应用的状态不连续
      
      在微任务处理完成之后,当且仅当微任务队列中没有正在等待中的 微任务,才可以重新渲染页面
      

      宏任务和微任务都是独立于事件循环的,也就是说任务队列的添加行为发生在事件循环之外,如果不这样,当JavaScript代码执行的时候会忽略任何发生的事件。

    • 单个任务的执行时间16ms

      浏览器通常会尝试每秒渲染60次页面,以达到每秒60帧(60 fps) 的速度。60fps通常是检验体验是否平滑流畅的标准,这意味着浏览器会尝试在16ms内渲染一帧。因此,理想情况下,单个任务和该任务附属的所有微任务,都应在16ms内完成。

      浏览器完成页面渲染,进入下一轮事件循环迭代后,会发生以下三种情况:
      1,执行时间小于16ms,事件循环执行到“是否需要进行渲染”的决策环节,没有显式地指定需要页面渲染,浏览器可能不会在本轮循环中执行UI渲染操作
      2.执行时间差不多为16ms,事件循环执行到“是否需要进行渲染”的决策环节此时,浏览器会进行UI更新,以便用户能够感受到顺畅的应用体验
      3.执行时间大于16ms,此时浏览器将无法以目标帧率重新渲染页面,且UI无法被更新,如果执行时间不超过几百ms用户可能察觉不到卡顿,如果好使过长,用户会察觉到页面卡顿无响应,严重时会卡死
      

web性能

  • DOM层面

    DOM的访问和修改都会造成性能上的消耗,因此操作DOM的代价是很昂贵的。所以提升web性能就要尽量减少DOM的访问和修改次数。

    1.修改页面内容尽量使用innerHTML。虽然用innerHTML属性和DOM原生方法二者性能相差无几,但是对于大多数浏览器而言(除WebKit内核的浏览)innerHTML的性能更好一些。
    2.更新页面内容使用element.cloneNode()替代document.createELement()。对于大多数浏览器而言,节点克隆都更有效率,但也并不是很明显。
    3.访问HTML节点集合时使用局部变量。因为HTML集合一直与文档保持连接,每次需要最新的信息时都会重复执行查询过程,包括读取集合的length属性。因此如果需要多次访问同一个DOM时,尽量把该DOM、DOM集合或者集合的length值用一个局部变量存储起来。
    4.在老版本的IE中推荐使用nextSibling来获取DOM元素。
    5.尽量使用诸如children、nextElementSibling等代替childNodes、nextSibling来查找元素节点。在所有的浏览器中children都比childNodes要快1.5-3倍。
    6.使用querySelectorAll()和querySelector()选择器API来代替document.getELementsByName()、document.getElementsByClassName()、document.getElementsByTagName()、document.getElementById7.要注意重绘和重排,重绘和重排会带来性能上的开销。最好避免使用offsetTop、scrollTop。
    8.最小化重绘和重排,应该批量化处理样式(利用cssText或者class)和批量修改DOM。
    9.批量修改DOM时,可以离线操作DOM即先让元素脱离文档流,然后对其应用多重改变,最后再把元素带回到文档中。
    10.缓存布局信息,以减少访问布局信息的次数。
    11.动画中使用绝对定位,让元素脱离动画流。
    12.在元素很多时避免使用hover。
    13.当页面中存在大量元素,如果每一个都要一次或多次绑定事件处理器会影响性能,因此可以使用事件委托(冒泡,捕获)来减少事件处理器的数量。
    
  • css样式表层面

    1.使用link标签将css样式表放在文档的head标签里。这样也会解决所谓的白屏和无样式内容的闪烁。
    

    虽然将css样式表放在文档的head标签里,会延迟页面中其他重要组件的加载,但是他对于加载页面所需的实际事件没有太多的影响。相反如果将css样式表放在文档的底部会造成“白屏”或者“无样式内容的闪烁问题”。

    • 白屏问题

      将样式表放在文档底部会导致浏览器中阻止内容逐步呈现。为避免当样式变化时重绘页面中的元素,浏览器会阻塞内容逐步呈现,因此浏览器会延迟显示任何可视化内容。
      在IE浏览器中,在新窗口中打开时、重新加载时、作为主页时,都会导致白屏。
      
    • 无样式内容的闪烁

      如果将css样式表放在文档底部,当浏览器选择延迟呈现,直到所有的样式表都下载完成之后,这会导致白屏。相反如果浏览器选择页面逐步加载,文字会先显示然后是图片,最后,在样式表正确地下载并解析之后已经呈现的文字和图片要用最新的样式重绘了。这就是“无样式内容闪烁”。
      在IE浏览器中如果是点击链接,使用书签或键入url,此时IE会选择“无样式内容闪烁”,而对于firefox无论哪种情况都会导致“无样式内容闪烁”。
      
    • 尽量使用link标签引入css样式表

      将css样式表放在文档中有两种方式:link标签和@import规则
      一个style块可以包含多个@import规则。但是@import规则必须放在其他 规则之前。不然@import规则中的样式表不会加载
      因为@import规则的下载顺序时无序的,即便把@import规则放在head标签里也可能会导致白屏。
      
  • javascript代码的加载和执行层面

    1.</body>闭合标签之前,将Javascript代码放到文档底部,可以保证脚本执行前页面已经完成渲染。
    2.合并脚本。减少页面中包含的<script>标签,把多个脚本文件合并成一个,这样只需要引入一个<script>标签,从而减少性能的消耗,原因是每个<script>标签初始化都会阻塞页面渲染。另外对于外链脚本来说,下载单个100KB的文件要比下载4个25KB的文件要快,因此要尽量较少外链脚本的数量。
    3.使用<script>标签的defer属性延迟脚本,此时不用考虑JavaScript脚本在文档中的顺序。
    4.使用<script>标签的async属性的异步脚本。
    5.动态创建<script>标签引入javascript文件,文件会在元素添加到页面时开始下载,下载和执行过程不会阻塞页面的其他进程。下载完成后会立即执行,如果页面中其他脚本执行依赖这个脚本,当其他脚本执行时这个脚本还没有下载执行完就会出错,因此可以用script.onload事件来确保脚本下载完成且准备就绪。如果要动态创建多个<script>标签到页面上,要确保它们的顺序,因为只有Firefox和Opera能保证脚本会按指定的顺序执行,其他浏览器会按照服务端返回的顺序下载执行代码。
    6.利用XMLHttpRequest对象获取脚本并注入到页面中。先创建一个XHR对象,然后用它下载JavaScript文件,最后通过创建动态的<script>元素,设置该元素的text属性为服务器接收到的responseText,把JavaScript代码注入到页面中。相当于创建了一个内联脚本的<script>标签,当新创建的<script>元素被添加到页面后代码开始执行。由于代码下载是在<script>标签还没有生成在页面时,因此这种方法的优点是:可以下载javascript但不立即执行,你可以决定让脚本何时执行。从而能保证脚本的执行顺序。局限性:JavaScript文件必须与所请求的页面处于相同的域,也就意味着JavaScript文件不能从CDN下载。
    
    上述3、4、5都属于无阻塞脚本的解决方案。
    

    如果把Javascript脚本放在页面顶部将会导致明显的延迟。JavaScript可以随意操作DOM,Javascript加载和执行时会阻塞页面。也会阻塞在它之后的文件的下载,并且不能选择和修改还没被创建的节点。这就是为什么要把script元素放在页面底部的原因。

    尽管把单个javascript文件只产生一次HTTP请求,但是如果文件过大,它下载的过程中会比较长,因此仍会锁死浏览器一段时间。因此我们需要在页面中逐步加载JavaScript文件。我们可以在window对象的load事件触发后(就DOM加载完成)再加载脚本,DOM加载会在onload事件被触发前完成。因此我们可以使用defer async来延迟脚本。

    DOMcontentloaded事件是只DOM加载完成,而不理会JavaScript文件、css文件、图像是否已经下载,而load事件会在页面中一切都加载完毕时触发。因此DOMcontentloaded事件始终会在load事件之前触发。

    script标签的defer和async属性及其区别:带有defer属性的标签可以放置在文档的任何位置,对应的JavaScript文件将在页面解析到script标签时开始下载,但并不会执行,直到DOM加载完成(DOMcontentloaded事件触发后),window对象的load事件触发前。当一个带有defer属性的JavaScript文件下载时,他不会阻塞浏览器的其他进程,因此它可以与页面中的其他资源并行下载。

    带有defer属性的script元素在DOM完成加载之前都不会执行,defer属性只适用于外部脚本文件。HTML规范要求延迟脚本按照它们出现的顺序执行,先于DOMContentLoaded事件执行,但是现实中延迟脚本不一定会顺序执行,也不一定会在DOMcontentLoaded事件触发前执行,因此最后只包含一个延迟脚本。

    带有async属性的script元素只适用于外部脚本文件,并告诉浏览器立即下载文件,并尽快执行脚本。异步脚本一定会在页面的load事件前执行,但可能会在DOMcontentloaded事件触发之前或之后执行,由于异步脚本在他们载入后就执行,因此标记为async的异步脚本可能不会按照它们指定的先后顺序执行,这取决于哪个脚本先下载完成。使用async异步脚本的目的是不让页面等待脚本的下载和执行,从而可以异步加载页面内容。因此建议不要再异步脚本加载期间修改DOM。另外,如果同时使用defer和async属性浏览器会采用async而忽略defer属性。

    • 浏览器UI进程

        用于执行JavaScript和更新用户界面的进程通常被称为“浏览器UI线程UI线程的工作基于一个队列系统,任务会保存到队列中,当进程空闲时,队列中的下一个任务就会被提取出来运行,这些任务要么是JavaScript代码要么是执行UI更新,包括重绘与重排
      
    • 浏览器对JavaScript脚本运行的限制

      1.对于JavaScript代码执行的总时间进行限制。
      2.对于JavaScript代码执行的语句数量进行限制。
      
      不同的浏览器对于检测时间会有不同,不同的浏览器对于一定量的脚本执行的时间长短也有差异。
      
    • 缩小JavaScript的执行时间来提升页面性能。

      浏览器单线程的工作机制,且JavaScript代码和UI渲染共用一个线程,因此javascript执行的时候UI渲染处于停滞状态,反之亦然
      
      避免影响用户体验可以采用以下方式来缩小JavaScript的执行时间
      1.任何JavaScript执行不能超过100ms,过长的运行时间会导致UI更新出现明显的延迟从而影响用户体验
      2.利用定时器来安排代码延迟执行,它使得你可以长时间运行脚本分解成一系列的小任务
      3.利用HTML5的新特性,Web Workers是新版浏览器支持的特性,他允许在UI线程外部执行JavaScript代码,从而避免锁定UI
      

      Web Workers

      • 由于Web Workers没有绑定UI线程,因此Web Workers能使JavaScript代码独立运行,且不占用浏览器UI线程的时间。

      • 每个新的Worker都在自己的线程中运行代码,因此,Worker运行代码不仅不会影响浏览器UI,也不会影响其他Worker中运行的代码。

      • Web Workers从外部线程中修改DOM会导致用户界面出错。每个Web Workers都有自己的全局运行环境。

      • web Workers适用于与浏览器无关的长时间运行脚本,或者处理纯数据。例如:编码、解码大字符串。处理复杂的数学运算、大数组排序等。

    Web Workers的使用方式

     
     // 1.在当前的页面比如main.js中传入这个JavaScript文件
      var worker = new Worker('code.js')
      // 此代码一旦执行,将为该文件创建一个新的线程和一个新的worker运行环境,当代码下载并执行完成后才会启动Worker
     // .通信 网页代码通过postMessage()给Workers传递数据,接收一个参数。
      worker.postMessage("Javascript")
      // 网页代码通过onmessage事件处理器接收信息
      worker.onmessage = function(event) {
          //data属性用于存放传入的数据
          console.log(event.data)
      }
      
      
      // 2.创建一个web Workers,也就是创建一个完全独立的JavaScript文件,其中就是需要在Worker中运行的代码。例如:这个文件为code.js 其内部代码如下:
      // Worker通过自己的onmessage方法接收数据数据存放在data中。
     self.onmessage = function(event) {
         // worker通过自己的postMessage()方法把处理后的数据回传给页面main.js
         self.postMessage("hello"+event.data)
     }
     
     // code.js内部可以加载外部的javascript文件
     importScripts("file1.js","file2.js")//他可以接收一个或多个JavaScript文件,调用过程是阻塞式的,直到所有文件加载并执行完成后,脚本才会继续运行,由于workers是运行在UI线程之外的,所以这种阻塞不会影响UI响应。