前端系统化学习【JS篇】:(十九)从<浏览器的渲染机制>到<CRP关键节点优化>

前言

  • 细阅此文章大概需要 15分钟\color{red}{15分钟}左右
  • 本篇中详细讲述\color{red}{详细讲述}了:
    1. 进程和线程
    2. 浏览器渲染机制的具体解析过程
    3. CRP关键节点优化方案总结
    4. 回流和重绘
    5. 现代浏览器的渲染队列机制
  • 如果有任何问题都可以留言给我,我看到了就会回复,如果我解决不了也可以一起探讨、学习。如果认为有任何错误都还请您不吝赐教,帮我指正,在下万分感谢。希望今后能和大家共同学习、进步。
  • 下一篇会尽快更新,已经写好的文章也会在今后随着理解加深或者加入一些图解而断断续续的进行修改。
  • 如果觉得这篇文章对您有帮助,还请点个赞支持一下,谢谢大家!

进程和线程:

  • 进程:

    • 一个程序(如浏览器新建一个页卡就产生一个进程)【可理解为一个工厂】
  • 线程:

    • 一个进程中可能包含多个线程,但每个线程同时可以做一件事【工人】
    • 想真正的同时做多件事情,必须依赖多线程
    • 浏览器是多线程的【浏览器异步:可以同时做多件事情】
    • JS是单线程的 【浏览器只分配一个线程去渲染js】
  • 浏览器中的线程:

    • GUI渲染线程 :自上而下渲染页面
    • JS引擎线程 :渲染和执行JS代码的
    • 事件触发线程 :事件绑定的时候,会有一个线程去监听事件是否触发,一旦事件触发,这个线程帮我们通知绑定的方法执行
    • 定时器触发线程 :设置定时器之后,分配一个线程去监听是否到达时间,当到时间后通知对应方法执行
    • 异步HTTP请求线程 :分配一个线程从服务器端获取内容【css/js/img/数据…】
    • webWorker
  • 【JS代码执行】 【单线程】【也是配合浏览器的多线程去实现一些异步操作】

    • 但是JS本身是单线程的(因为浏览器本身只分配一个线程运行JS代码)
    • JS本身从本质来讲是不能同时做多件事情的【同步和异步的特点】【JS中的异步并不是同时做多件事情】
    • JS单线程也可以实现异步效果,是利用浏览器多线程来完成的
      • 【同步】:上一件事情完成,再去做下一件事情
      • 【异步】:上一件事情没有完成(我们做一些处理(利用浏览器的多线程))下一件事情继续执行
  • 【浏览器生成DOM树、CSSOM树…的过程】 【本身也是单线程的】【也是配合浏览器的多线程去实现一些异步操作】

    • 浏览器生成DOM TREE、CSSOM-TREE…是’GUI渲染线程’逐流程进行的,过程是单线程的
    • 例如资源请求:就是利用的就是浏览器的HTTP线程(网络线程)去做的

浏览器渲染机制的具体解析过程:

1. 【预先做的事】当浏览器从服务器获取到HTML页面(代码)后,会分配一个GUI渲染线程,自上而下的去渲染解析代码

  1. 如果遇到 <link>\<img>\<audio>\<vedio>等 标签 ,分配一个新的线程(HTTP请求线程)去获取资源文件,与此同时GUI会继续向下渲染(无需管资源是否回来)“不阻碍GUI渲染!!!”
  2. 如果遇到<style> ,GUI继续渲染代码,此时把内嵌样式就解析渲染了
  3. 如果遇到@import "xxx.css" ,此时GUI停止渲染,分配一个新的HTTP线程去获取资源文件,必须等到资源文件回来,GUI才会继续渲染,把获取的css代码进行解析…. “会阻碍GUI的渲染!!!!”
    • 【对应优化】 项目中尽可能不要使用 @import 【排除less和sess中的import,因为在这些代码编译完成的时候 @import 就没有了】
    • 【对应优化】 如果css代码较少,直接使用内嵌式即可,减少http请求次数,让代码渲染的更及时【页面第一次打开速度就更快】 如果代码比较多,还是用外链式来单独导入样式吧吧,且最主要的是代码方便维护,也不至于请求HTML页面就要很长时间

2. 【第一步】自上而下解析完成所有的HTML标签/各种节点后,生成DOM TREE

  • 渲染和解析DOM结构,规划好节点和节点之间的关系
  • 在DOM TREE生成之后,会触发一个事件DOMContentLoaded
  • 【优化】避免过深的DOM层级嵌套

3. 【第二步】等待所有CSS资源请求回来之后,按照导入的先后顺序,依次渲染CSS样式,要保证CSS渲染顺序和优先级等问题,生成CSSOM TREE

  • 【优化】我们把<link>放在HEAD标签中,在渲染DOM之前就发送资源请求,这样等到DOM TREE生成后,可能css资源已经请求回来了,此时直接渲染即可….(让事情同时去做,可以提高页面第一次渲染的速度)
    • 由于页面同时并发的HTTP请求的数量是5~7个 【限定同源】
    • 而且过多的请求会导致网络通道阻塞,还有很多其他原因,导致 多次发送HTTP请求,不如只发送一次请求快
    • 所以在项目中,我们需要把所有的CSS资源合并为一个文件 !!【而且这样也就无需再考虑多个资源文件的加载顺序和渲染顺序问题】

4. 【第三步】把生成的 DOM TREECSSOM TREE 合并成为 RENDER TREE【渲染树】

  • 渲染树中包含了浏览器最终渲染时,每个节点应该具备的样式 【包含自己本身的样式,和继承自父级元素样式,浏览器默认的样式等】

5. 【第四步】浏览器按照RENDER TREE开始渲染

  1. layout【布局】 :根据当前viewport视口大小计算出每个节点在视口当中的位置
  2. 【分层】 :构建每一层的文档流,并且规划好每一层如何绘制【绘制的步骤都计算好】
  3. painting【绘制】 :按照分析好的规则,开始绘制页面,最后在浏览器的视口中呈现出我们要的页面

6. 【JAVASCRIPT】在页面渲染的过程中,遇到了<script>资源请求

  • 默认情况下遇到<script>资源请求,都会阻碍GUI的渲染
    1. 发送HTTP请求,获取资源文件,此时阻碍GUI渲染
    2. 资源获取到之后,交给JS引擎线程去渲染和解析JS
    3. JS解析完成,GUI继续
  • 但是如果给<script>设置defer或async,则不会阻碍GUI渲染
    1. <script>:资源请求和执行JS都会阻碍GUI
    2. <script async>:资源请求不会阻碍GUI【开辟新的HTTP线程去请求,GUI继续渲染】,当资源请求回来后,会立即渲染解析JS,此时中断GUI渲染,当JS执行完,GUI才会继续
    3. <script defer>:资源请求不会阻碍GUI【开辟新的HTTP线程去请求,GUI继续渲染】,不管资源何时请求回来,资源请求回来后,都不会立即渲染解析JS,都要等到GUI渲染完成,最终按照导入顺序执行JS【和link特别像】
    4. async和defer的区别,async不会考虑js顺序关系,谁先请求回来谁先执行;但是defer需要等到所有请求的资源都回来,GUI渲染完成了,再去按照依赖的顺序执行js

优化方案总结:

  1. 避免过深的DOM层级嵌套 ———> 加快DOM TREE的生成
  2. CSS选择器渲染顺序是从右往左,所以 CSS选择器避免前缀过长 ——> 加快CSSOM TREE的生成
  3. 优先使用<style> 内嵌样式 ———> 减少HTTP请求&加快CSS渲染
  4. 样式过多的情况下使用<link> ,但是把所有CSS资源合并为一个CSS样式文件【webpack可以自动打包】———>减少HTTP请求&link并不会阻碍GUI的渲染
  5. <link> 放在页面头部 ————>创建DOM TREE的同时,去请求资源文件
  6. 坚决不要@import【排除sess和less】
  7. 一般把<script>放在页面末尾,若非要放在顶部,最好设置defer或async ———>防止其阻碍GUI的渲染
  8. 项目中,也需要 把所有的js资源合并为一个js文件 ,也是为了减少http请求。
  • 优化的目的就是为了让页面渲染速度更快,白屏等待时间更短

  • 回流和重绘

  • layout【回流/重排】

    • 页面第一次加载,必然会经历一次 layout【回流/重排】 ,计算出每个节点在视口中的位置 【首次加载】
    • 当会引发重排的行为出现时,浏览器需要重新计算每个节点在视口中的位置(或者重新计算布局信息),也就是重新layout一次,这样的操作非常消耗页面渲染性能,这就是重排或回流。
    • 重排必然会引发重绘,重绘不会引发重排
    • 【引发回流的某些情况】

      • DOM元素的增删改 导致DOM结构发生变化【DOM结构改变】
      • DOM的样式(如:大小位置等)发生改变【DOM样式改变】
      • 内容的变化,导致尺寸变化【DOM样式改变】
      • 浏览器窗口大小发生改变,也会引发回流 【视图/视口改变】
    • 【避免DOM回流的几种方式】

      1. 【首先:放弃传统的操作DOM】,使用VUE、react等框架,基于数据驱动,实现视图的渲染【本质:框架本身把DOM操作进行了封装,在内部实现了对DOM的优化处理】
      2. 【读写分离操作】 【基于现代浏览器的渲染队列机制】
        • 在真实项目中对DOM元素样式,我们应该’读写分离’:把 【设置样式】【获取样式】 的操作 【分开】 , 在整体都设置完之后,在进行样式的获取,从而降低回流的次数
      3. 【元素批量修改】 【临时容器文档碎片】【拼接字符串】
        • createDocumentFragment 创建文档碎片

          • 创建一个文档碎片,相当于一个临时容器,动态创建元素先都放进临时容器中,暂时不放进页面DOM结构当中,就不会引发页面回流,当最后动态修改结束时,统一进行添加,就会尽可能地减少回流次数
              //创建一个文档碎片,相当于一个临时容器,创建五个span先都放进临时容器中,暂时不放进页面DOM结构当中,就不会引发页面回流
              //最终只会引发一次回流
              let frag = document.createDocumentFragment();//创建一个文档碎片
              for(let i=1; i<=5; i++){
                      let span = document.createElement('span');
                      span.innerHTML = i;
                      farg.appendChild(span);//创建五个span先都放进临时容器中,暂时不放进页面DOM结构当中
                  }
                  box.appendChild(frag);//一次添加到box中
                  frag = null;//清空文档碎片
          复制代码

        • 字符串拼接【涉及到浏览器底层解析,所以性能差于文档碎片】

              for(let i=1; i<=5; i++){
                  str+=`<span>${i}</span>`;//将循环添加的span拼接为字符串
                  }
                  box.innerHTML = str;//一次添加到box中
                  // 最终只会引发一次回流
          复制代码

      4. 【缓存布局信息】【基于读写分离】
      5. 【样式集中改变】 【与批量修改同理】【div.style.cssText = ‘width:10px;height:10px;…’】【div.className = ‘box’;】
      6. 分层以加速回流 【未发生改变的层不会进行回流】
      7. 使用CSS3硬件加速(GPU加速)
        • 修改元素的transform、opacity…样式不会引发原始文档流中的DOM重排。
        • 修改元素的transform,会把元素单独脱出一个文档流,后期浏览器只是重新计算这个文档流中的位置和布局,对原始的其他文档流不会有任何影响…
        • opacity除了变透明了看不见了,但是在结构中还存在,所以不会引发回流
        • transform 【跳过render-tree和layout】 直接通知合成线程渲染,只会引发重绘
        • 同样的,我们后期修改元素的样式,尽可能修改那些脱离文档流的元素样式,这样后期重新计算布局信息的时候,也只是对这层文档流重新计算,总比全部重新计算好得多。
  • 重绘【不可避免的】

    • 页面第一次渲染必然会经历一次 painting 绘制,以此来绘制出页面
    • 元素的样式发生改变(但是几何信息和结构信息宽高、大小、位置等不变) 此时不需要回流,只需要浏览器把改变的元素重新渲染即可
    • 【引发重绘的情况】
      • color
      • background-color
      • background-image【主要是看背景图片发生改变之后,有没有让当前容器/盒子的几何信息发生改动,改变了就回流,没有改变就是重绘】
      • 动画 transform
      • ….
  • 平时所说的’操作DOM耗费性能’,大部分指的是DOM重排!!!所以减少DOM重排,是前端性能优化的重要指标

  • 现代浏览器中的【浏览器的渲染队列机制】:

    1. 上一行代码是 修改元素样式,此时并 【没有直接通知浏览器去渲染】 ,而是将它 放置到【浏览器的渲染队列】当中 并继续向下执行
    2. 每当遇到修改样式的操作,全部放置到浏览器的渲染队列当中
    3. 【当不再有修改样式的操作,或者遇到了获取样式的操作(盒子模型或获取样式),则中断向队列中存放的操作,把现有的队列中的对样式的操作先渲染一次(引发一次回流)】
    4. 接着继续向下执行代码,若再次遇到样式修改,则再次执行如上操作…

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享