02|能让我想想吗?url到页面渲染相关知识

272 阅读13分钟

前言

最近比较忙,所以更新有点慢,请大家谅解!接着为大家介绍面试深挖的知识,相关知识点的提词器内容!

从输入url到页面显示到底发生了什么?

这道题是必考题,大家要值得注意!面试官可以从这道题深挖很多知识点出来,你需要做好准备。

下载.jpg

当我们拿到url的时候,会进行DNS域名解析拿到域名,再拿到服务器的IP,基于IP层以上的运输层建立TCP连接,然后服务器端返回的html就可以传输了,在http2.0下多路复用TCP连接,我们可以并行获取CSS资源等,由此进入浏览器渲染流程

  1. 输入URL后会进行DNS域名解析,找到对应的IP地址(什么是DNS?DNS怎么解析的?)
  2. 建立TCP连接(三次握手和四次挥手知识)
  3. 服务端返回html资源
  4. 开始解析html,完成DOM树的构建
  5. 接着解析CSS,完成CSSDOM树的构建()
  6. 合并DOM树和CSSOM树(CSS规则树),生成 Render树(渲染树)
  7. GUI线程完成像素级别绘制
  8. layout网页布局生效
  9. BFC自上而下布局

url到页面渲染(上)

定位

DNS域名解析 => IP协议 => 建立TCP连接(三次握手,四次挥手)

url到页面渲染(下)

从 8 道面试题看浏览器渲染过程与性能优化

浏览器渲染过程与性能优化

前端性能优化之关键路径渲染优化

浏览器多进程架构

多进程浏览器将浏览器的各种不同类别的任务拆分出来,放到多个不同的进程中去执行。每个进程中又包含多个线程,一个进程内的多个线程也会协同工作,配合完成所在进程的职责。

渲染进程的所有线程 image.png

GUI渲染线程
  • 负责渲染浏览器界面
    • 解析HTML,CSS
    • 构建DOM树和CSSOM树
    • 将DOM树和CSSOM树合成为RenderObject树
    • 布局和绘制
  • 负责执行重绘回流的操作
  • GUI 渲染线程与 JS 引擎线程是互斥的,当 JS 引擎执行时 GUI 线程会被挂起(相当于被冻结了),GUI 更新会被保存在一个队列中等到 JS 引擎空闲时立即被执行。
JS引擎线程
  • JavaScript引擎,也称为 JS 内核(例如 V8 引擎)

  • 负责处理 JavaScript脚本程序。

  • JS 引擎一直等待着任务队列中任务的到来,然后加以处理

    一个 Tab 页(renderer 进程)中无论什么时候都只有一个 JS 线程在运行 JS 程序。

  • GUI 渲染线程与 JS 引擎线程是互斥的,所以如果 JS 执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。

事件触发线程
消息队列
  • 宏任务
  • 微任务
网络异步线程
定时器线程

关键渲染路径

浏览器接收到服务器返回的HTMLCSSJavaScript字节数据并对其进行解析和转变成像素的渲染过程被称为关键渲染路径。通过优化关键渲染路径即可以缩短浏览器渲染页面的时间。

  1. 解析HTML 文件,构建 DOM 树,同时浏览器主进程负责下载 CSS 文件(不阻塞DOM树的解析,但阻塞DOM树的渲染)

    bytes => characters => tokens => nodes(objects) => dom/cssom

    字节数据 => 编码 => 令牌化 => 生产对象 => 生成树

    • 编码: 先将HTML的原始字节数据转换为文件指定编码的字符。

    • 令牌化: 然后浏览器会根据HTML规范来将字符串转换成各种令牌(如<html><body>这样的标签以及标签中的字符串和属性等都会被转化为令牌,每个令牌具有特殊含义和一组规则)。令牌记录了标签的开始与结束,通过这个特性可以轻松判断一个标签是否为子标签(假设有<html><body>两个标签,当<html>标签的令牌还未遇到它的结束令牌</html>就遇见了<body>标签令牌,那么<body>就是<html>的子标签)。

    • 生成对象: 接下来每个令牌都会被转换成定义其属性和规则的对象(这个对象就是节点对象)。

    • 构建完毕: DOM树构建完成,整个对象集合就像是一棵树形结构。可能有人会疑惑为什么DOM是一个树形结构,这是因为标签之间含有复杂的父子关系,树形结构正好可以诠释这个关系(CSSOM同理,层叠样式也含有父子关系。例如: div p {font-size: 18px},会先寻找所有p标签并判断它的父标签是否为div之后才会决定要不要采用这个样式进行渲染)

  2. CSS 文件下载完成,解析 CSS 文件成树形的数据结构,然后结合 DOM 树合并成 RenderObject 树(渲染树)

不可见的节点自然就没必要渲染到页面了,不可见的节点还包括被CSS设置了display: none属性的节点,值得注意的是visibility: hidden属性并不算是不可见属性,它的语义是隐藏元素,但元素仍然占据着布局空间,所以它会被渲染成一个空框)。

  1. 绘制 RenderObject 树 (paint),绘制页面的像素信息

  2. 布局(BFC)

  3. 浏览器主进程将默认的图层和复合图层交给 GPU 进程,GPU 进程再将各个图层合成(composite),最后显示出页面。

注意

DOM树和CSSOM树是并行解析的,CSS加载不会阻塞DOM树的解析,但是会阻塞DOM树的渲染,DOM树必须等到CSSOM树渲染完成(也就是 CSS 资源加载完成或者 CSS 资源加载失败后)才能开始渲染,然后一起合成渲染树

为什么CSS或JS会阻塞渲染?
  • JS

    由于 JavaScript 是可操纵 DOM 的,如果在修改这些元素属性同时渲染界面(即 JavaScript 线程和 UI 线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。

    因此为了防止渲染出现不可预期的结果,浏览器设置 GUI 渲染线程(DOM/CSSOM解析渲染)与 JavaScript 引擎为互斥的关系。

    当浏览器在执行 JavaScript 程序的时候,GUI 渲染线程会被保存在一个队列中,直到 JS 程序执行完成,才会接着执行。

  • CSS

    DOM树和CSSOM树是并行解析的,CSS加载不会阻塞DOM树的解析,但是会阻塞DOM树的渲染,DOM树必须等到CSSOM树渲染完成(也就是 CSS 资源加载完成或者 CSS 资源加载失败后)才能开始渲染,然后一起合成渲染树

    CSS会阻塞DOM树的渲染,也会阻塞JS的执行。

DOMContentLoaded 与 load 的区别 ?
  • 当文档中没有脚本时,浏览器解析完文档便能触发 DOMContentLoaded 事件。如果文档中包含脚本,则脚本会阻塞文档的解析,而脚本需要等 CSSOM 构建完成才能执行。在任何情况下,DOMContentLoaded 的触发不需要等待图片等其他资源加载完成。
  • 当 onload 事件触发时,页面上所有的 DOM,样式表/脚本/图片等资源已经加载完毕。
  • DOMContentLoaded => load
DOM加载 | CSS文件 | JS文件加载顺序

构建:解析 + 渲染

  • 解析HTML文件,开始构建DOM树

  • 遇到<link>标签,浏览器会发送请求获得该标签中标记的CSS文件(下载CSS文件),此时不会阻塞DOM树解析(的这个过程),但是会阻塞DOM树的渲染(如果解析完了)

  • CSSOM树构建完成

  • 继续构建DOM树

  • 遇到<script>标记,暂停构建DOM,JavaScript引擎(线程)开始执行JavaScript脚本,此时阻塞GUI渲染线程(DOM/CSSOM构建),直到执行结束后,浏览器才会从之前中断的地方恢复,然后继续构建DOM

    每次去执行JavaScript脚本都会严重地阻塞DOM树的构建,如果JavaScript脚本还操作了CSSOM,而正好这个CSSOM还没有下载和构建,浏览器甚至会延迟脚本执行和构建DOM,直至完成其CSSOM的下载和构建。
    
    所以当我们把script标签放到link标签之前,会发现我们拿不到DOM结构,如果操作了CSS,就会构建CSSOM,合成渲染树,然后布局,渲染。我们可以看到DOM的布局但是打印不了我们所要的信息
    
    所以为什么我们一般会在body标签的末尾写script标签就是这个原因,
    或者使用window.onload()/DOMContentLoaded/load;
    
渲染阻塞的优化方案
优化CSS
  • CSS媒体查询

    CSS资源只在特定条件下使用,这样这些资源就可以在首次加载时先不进行构建CSSOM树,只有在符合特定条件时,才会让浏览器进行阻塞渲染然后构建CSSOM树。

    <!-- 动态媒体查询, 将在网页加载时计算。根据网页加载时设备的方向,portrait.css 可能阻塞渲染,也可能不阻塞渲染。-->
    <link href="portrait.css" rel="stylesheet" media="orientation:portrait"> 
    <!-- 只在打印网页时应用,因此网页首次在浏览器中加载时,它不会阻塞渲染。 --> <link href="print.css" rel="stylesheet" media="print">
    

    使用媒体查询可以让CSS资源不在首次加载中阻塞渲染,但不管是哪种CSS资源它们的下载请求都不会被忽略,浏览器仍然会先下载CSS文件

优化JavaScript
  • async

    HTML的解析与脚本资源的下载是异步的,(下载完后马上执行)但是与JS执行的时候与GUI线程是互斥的。

  • defer

    HTML的解析与脚本资源的下载是异步的,下载完后,在所有元素解析完成之后,DOMContentLoaded 事件触发之前执行脚本。

image.png

  • 关于 defer,此图未尽之处在于它是按照加载顺序执行脚本的,这一点要善加利用
  • async 则是一个乱序执行的主,反正对它来说脚本的加载和执行是紧紧挨着的,所以不管你声明的顺序如何,只要它加载完了就会立刻执行。不依赖于任何脚本,也不被任何脚本依赖。
其他优化方案
  • 加载部分HTML

    服务端在接收到请求时先只响应回HTML的初始部分,后续的HTML内容在需要时再通过AJAX获得。由于服务端只发送了部分HTML文件,这让构建DOM树的工作量减少很多,从而让用户感觉页面的加载速度很快。

    注意,这个方法不能用在CSS上,浏览器不允许CSSOM只构建初始部分,否则会无法确定具体的样式。

  • 压缩

    利用 GZIP 压缩文件。

  • HTTP缓存

    • ETag

      ETag是一个传递验证令牌,它对资源的更新进行检查,如果资源未发生变化时不会传送任何数据。

      当浏览器发送一个请求时,会把ETag一起发送到服务器,服务器会根据当前资源核对令牌(ETag通常是对内容进行Hash后得出的一个指纹),如果资源未发生变化,服务器将返回304 Not Modified响应,这时浏览器不必再次下载资源,而是继续复用缓存。

    • Cache-Control

      Cache-Control定义了缓存的策略,它规定在什么条件下可以缓存响应以及可以缓存多久。

  • 资源预加载

    Pre-fetching是一种提示浏览器预先加载用户之后可能会使用到的资源的方法。

    • dns-prefetch

      提前进行DNS解析,以便之后可以快速地访问另一个主机名

      <link rel="dns-prefetch" href="other.hostname.com">
      
    • prefetch

      预先下载资源,优先级是最低的。

      <link rel="prefetch"  href="/some_other_resource.jpeg">
      
    • subresource

      预先下载资源,优先级是最高的。

      <link rel="subresource"  href="/some_other_resource.js">
      
    • prerender

      预先渲染好页面并隐藏起来,之后打开这个页面会跳过渲染阶段直接呈现在用户面前(推荐对用户接下来必须访问的页面进行预渲染,否则得不偿失)。

      <link rel="prerender"  href="//domain.com/next_page.html">
      
  • preload

    加速样式表/脚本/字体/其他资源的下载

    <link href="critial.css" rel="stylesheet" />
    <link rel="preload" href="non-critial.css" as="style" />
    <link href="non-critial.css" rel="stylesheet" />
    
    <link rel="preload" href="//cdn.staticfile.org/jquery/3.2.1/jquery.min.js" as="script" />
    
  • preload 会提升资源的优先级因为它标明这个资源是本页肯定会用到 —— 本页优先

    prefetch 会降低这个资源的优先级因为它标明这个资源是下一页可能用到的 —— 为下一页提前加载

回流(Reflow)

(会导致浏览器重新渲染的操作)

当 Render Tree 中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流。

会导致回流的操作:

  • 页面首次渲染
  • 浏览器窗口大小发生改变
  • 元素尺寸或位置发生改变
  • 元素内容变化(文字数量或图片大小等等)
  • 元素字体大小变化
  • 添加或者删除可见DOM元素
  • 激活CSS伪类(例如::hover
  • 查询某些属性或调用某些方法

一些常用且会导致回流的属性和方法:

  • clientWidthclientHeightclientTopclientLeft
  • offsetWidthoffsetHeightoffsetTopoffsetLeft
  • scrollWidthscrollHeightscrollTopscrollLeft
  • scrollIntoView()scrollIntoViewIfNeeded()
  • getComputedStyle()
  • getBoundingClientRect()
  • scrollTo()

作者:腰花 链接:juejin.cn/post/684490… 来源:掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

如何避免
CSS
  • 避免使用table布局。
  • 尽可能在DOM树的最末端改变class
  • 避免设置多层内联样式。
  • 将动画效果应用到position属性为absolutefixed的元素上。
  • 避免使用CSS表达式(例如:calc())。
JavaScript
  • 避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性。
  • 避免频繁操作DOM,创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中。
  • 也可以先为元素设置display: none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘。
  • 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
  • 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。

重绘 (Repaint)

当页面中元素样式的改变并不影响它在文档流中的位置时(例如:colorbackground-colorvisibility等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。

回流VS重绘

回流比重绘的代价要更高。

  • 仅回流一个单一的元素,它的父元素以及任何跟随它的元素也会产生回流。
  • 对回流与重绘操作进行优化

html书写方式的性能优化

  1. css用单独的类名,减少类,标签的嵌套,保持扁平化

    减少递归渲染,同理html不要嵌套没有意义的标签

    span {
        color: red;
    }
    /* 浏览器会先找所有span,然后再找装在a标签里面的span,还要找被a标签包裹而且被div包裹的span,需要递归寻找*/
    div > a > span {
        color: red;
    }
    

为什么操作DOM性能很差?

  1. 操作dom会导致多线程并发

    浏览器存在渲染引擎和js引擎,当用js操作dom时(document.getElementById),需要两个引擎之间通信,也就是多线程通信,多线程并发工作,造成性能开销大

  2. 可能引起重绘和回流