浏览器工作原理与实践(五)——浏览器页面渲染

Waxiangyu

DOM树构建

HTML解析器不是等文档加载完成才解析的,而是边加载边解析。
网络进程根据收到的content-type=text/html判断是html类型文件,为该请求创建一个渲染进程,渲染进程准备好后会在网络进程和渲染进程中建立一个共享数据的管道,网络进程接收到的字节流像水一样的倒进这个管道,渲染进程的html解析器会动态接收字节流将其解析为DOM

字节流Bytes——>分词器Tokens——>生成节点Node——>DOM

  1. 通过分词器将字节流转换成Token。分为Tag Token(StartTag、EndTag)和文本Token
  2. Token解析为DOM节点,添加到DOM树。Html中有一个Token栈结构,第一步中生成的Token都会进入这个栈中。
    如果是StartTagHtml解析器会为Token创建一个Dom节点,然后加入到DOM树中。
    如果是文本Token,会生成一个文本节点加入DOM树中,不用压入栈。
    如果是EndTagHtml会查看栈顶元素有没有它的StartTag,有就从栈顶弹出,表示该元素解析完成。

HTML 解析器开始工作时,会默认创建了一个根为 document 的空 DOM 结构,同时会将一个 StartTag documentToken 压入栈底。然后经过分词器解析出来的第一个 StartTag html Token 会被压入到栈中,并创建一个 htmlDOM 节点,添加到 document

首次加载白屏时间

如果在2段div中插入script,解析到<script>会暂停DOM解析。JS脚本执行完成后,Html解析器恢复继续执行。
渲染进程接收字节流时,也会开启一个预解析线程,遇到JSCSS文件,预解析线程会提前下载这些数据。请求Html到构建DOM之间有一段空闲时间,构建DOM树之后css未下载完之前也会有一段空闲时间,可能会有白屏。

<html>
    <head><link href="them.css" rel="stylesheet" /></head>
    <body>
        <div>segmentfault.com</div>
        <script src="foo.js"></script>
        <div>思否</div>
    </body>
</html>

image.png
解析Html、下载CSS、下载JS、生成CSSOM、执行JS、生成布局树、绘制页面。
主要空白:下载CSS、下载JS、执行JS
策略:1.内联JScss来移除这2种文件下载。2.减少文件大小、webpack移除注释,压缩JS文件。3.JSdeferasync。4.拆分css文件
优化:

  1. 加载阶段:图片,音频,视频文件不会阻塞页面首次渲染。JShtmlcss文件会阻塞。因为构建DOM需要HtmlJS,构建渲染树需要CSS
    (1). 减少关键资源个数:关键资源个数越多,首次加载时间越长。1.jscss改成内联。2.JS没有DOMCSSOM操作,加deferasynccss不是构建页面之前加载的加媒体取消阻止显现标志madio。他们就变成非关键资源了。
    (2). 降低资源大小:资源越小,下载时间越短。压缩css,js资源。移除注释。
    (3). 降低RTT数量:传输分包,小于14k只用1个RTTRTT次数越多,请求时间越长。减少关键资源个数或减少关键资源大小、CDN减少RTT时长。
  2. 交互阶段:JS脚本,渲染进程渲染帧的速度。交互阶段,帧的渲染速度决定了交互的流畅度。
    (1). 减少JS脚本执行时间:1.执行函数分解多个任务。2.web workers主线程之外的一个线程,可以执行js,无法操作DOMCSSOM。不要霸占主线程太久。
    (2). 避免强制同步布局:通过DOM接口,添加或删除元素,需要重新计算样式和布局。同步布局:JS强制将计算样式和布局操作提前到当前任务中。如加个元素,然后获取此元素高度,获取就要布局后才能获取。修改DOM前查询相关值。
    (3). 避免布局抖动:一次JS执行中,多次强布局和抖动。如for循环中,不断读取属性值,每次读都要进行计算样式和布局。不要在修改DOM时再去查相关值。
    (4). 合理利用css合成动画,css合成动画在合成线程上执行。不受主线程限制,尽量用css合成动画。
    (5). 避免频繁垃圾回收,垃圾回收时会占用主线程。优化存储结构,避免小颗粒对象的产生。

DOM的缺陷:
DOM提供了一组JS接口来遍历或修改节点。JS操作DOM会影响到整个渲染流水线。如document.body.appendChild(node),往body节点添加一个元素,首先渲染引擎将node添加到body之上,然后触发样式计算、布局、绘制、栅格化,合成等任务。强制同步布局和布局抖动问题,会大大降低渲染效率。
渲染主线程:用户输入事件、合成任务、定时器、V8垃圾回收、网络加载、HTML解析,布局等、JS回调。

页面渲染

chromium目前采取的任务高度。
加载阶段:默认——>用户交互——>合成页面——>空闲:尽可能看到页面,页面解析,JS优先级最高
用户交互:用户交互——>合成页面——>默认——>空闲
空闲阶段:默认,用户交互——>空闲——>合成页面

一条完整的流水线:解析HTML文件生成DOM、解析CSS生成CSSOM、执行JS、样式计算、构造布局树、准备绘制列表、光栅化、合成显示等一系列操作。

显卡中有一块叫前缓冲区的地方,显示器会每间隔1/60秒就读取一次前缓冲区,如果浏览器要更新显示的图片,浏览器会将新生成图片提到显卡的后缓冲区,提完之后,GPU将后、前缓冲区互换位置,就保证了显示器读取最新的图片。但显示器读取图片和浏览器生成图片不一定都同步的,不同步会造成掉帧、卡顿、不连贯问题
当显示器将一帧画面绘制完成后,并主任务读取下一帧之前,显示器会发一个垂直同步信号给GPU,简称vsync。当GPU接收vsync信号后,会将vsync信号同步给浏览器进程,浏览器进程将其同步到对应渲染进程,准备绘新一帧。
image.png
当渲染进程接收到用户交互任务的,要进行合成操作,这时可以设置当执行交互的任务时,将合成操作的任务优先级调到最高。
处理完DOM,计算好布局和绘制,就要将合成线程合成最终图了。合成线程在工作时,就可以把下个合成任务优先级调最低。将页面解析、定时器等任务优先级提升 ,如果合成操作非常快,那就有空闲时间了,可以执行如V8垃圾回收,或通过window.requestIdleCallback()设置的回调任务等。
为了解决一直执行高优先级任务,不执行低优先级任务的问题,还给每个队列设置了执行权重,连续执行了一定个数的高优先级任务,中间会执行一次低优先级任务。

虚拟DOM

  • 将页面改变的内容应用到虚拟 DOM 上,而不是直接应用到 DOM 上。
  • 变化被应用到虚拟 DOM 上时,虚拟 DOM 并不急着去渲染页面,而仅仅是调整虚拟 DOM 的内部状态,这样操作虚拟 DOM 的代价就变得非常轻了。
  • 在虚拟 DOM 收集到足够的改变时,再把这些变化一次性应用到真实的 DOM 上。

虚拟DOM通过JSX和基础数据创建虚拟DOM,由虚拟DOM创建出真实DOM树,再触发渲染流水线输出屏幕。
如果发生改变,就根据新数据创建新的虚拟DOM树,然后比较2棵树,找出变化,再把所有变化一次性重新更新真实DOM树上,更新渲染流水线,生成新页面。
react之前的递归算法比较多的话会占主线程比较久,造成卡页面。Fiber又叫协程,执行算法的过程中让出主线程,解决占用时间久的问题。双缓存,先将计算的中间结果放在另一个缓存区,等全部计算完再把结果应用到DOM上,减少不必要更新,保证DOM的稳定输出。

WebComponent

渲染引擎会将所有的css内容解析为CSSOM。生成布局树的时候,会在CSSOM中为布局树元素查找样式,2个相同标签显示的效果会是一样的,渲染引擎不会为他们单独设置样式。
css会影响到DOM中其它同样标签的样式。JS也能在任何地方修改DOM。是阻碍组件化的2个因素。

webComponent是一套技术的组合,具体涉及到Custom elements(自定义元素)、Shadow DOM(影子DOM)、HTML templates(Html模板)。
webComponent提供了对局部视图封装能力,可以让DOMCSSOMJS运行在局部环境,不会影响到全局。
微服务框架qiankun就是用的这个。
新建一个组件:1.定义模板。2.定义内部css样式。3.定义JS行为

<!DOCTYPE html>
<html>
  <body>
    <template id="template">
      <style>
        p{
          background-color: brown;
          color: cornsilk;
        }
        div{
          width: 200px;
          background-color: bisque;
        }
      </style>
      <div>
        <p>text1</p>
        <p>text2</p>
      </div>
      <script>
        function foo(){
          console.log('inner log')
        }
      </script>
    </template>
    <script>
      class ShadowDomTemplate extends HTMLElement{
        constructor(){
          super();
          //获取组件模板
          const content=document.querySelector('#template').content;
          //创建影子DOM节点
          const shadowDom=this.attachShadow({mode:'open'});
          //将模板添加到影子DOM上
          shadowDom.appendChild(content.cloneNode(true));
        }
      }
      customElements.define('temp-component',ShadowDomTemplate);
    </script>
    <temp-component></temp-component>
  </body>
</html>

渲染出:

<!DOCTYPE html>
<html>
  <head></head>
  <body>
    <template id="template">
      #document-fragment
    </template>
    <script></script>
    <temp-component>
      #shadow-root(open)
      <style>...</style>
      <div>...</div>
    </temp-component>
  </body>
</html>

影子DOM元素对网页不可见。
影子DOM的作用将模板中内容与全局DOMcss进行隔离,实现元素和样式私有化。可以把影子DOM看成一个作用域,内部样式和元素不会影响全局。全局下要访问影子内样式元素,通过约定好的接口,互不影响。
只通过影子DOM隔离cssDOM,但不会隔离JSJS可以被访问,渲染引擎判断temp-componentshadow-root,如是影子DOM会跳过,样式也只会从内部找。

阅读 299

那就 javascript 吧
小白笔记,从一开始。
628 声望
28 粉丝
0 条评论
628 声望
28 粉丝
文章目录
宣传栏