浏览器中一些有趣的知识

978 阅读16分钟

前言

在看完浏览器渲染原理之后引申出的一些疑问,这篇文章对这些疑问进行解答和梳理。

如果不熟悉浏览器渲染原理,可以看下我之前总结的文章 👉 浏览器渲染原理 👈

参考文章

HTML 解析器是等整个 HTML 文档加载完成之后开始解析的,还是随着 HTML 文档边加载边解析的?

HTML 解析器并不是等整个文档加载完成之后再解析的,而是网络进程加载了多少数据,HTML 解析器便解析多少数据

详细流程如下,网络进程接收到响应头之后,会根据响应头中的 content-type 字段来判断文件的类型,比如 content-type 的值是“text/html”,那么浏览器就会判断这是一个 HTML 类型的文件,然后为该请求选择或者创建一个渲染进程。渲染进程准备好之后,网络进程和渲染进程之间会建立一个共享数据的管道,网络进程接收到数据后就往这个管道里面放,而渲染进程则从管道的另外一端不断地读取数据,并同时将读取的数据“喂”给 HTML 解析器。你可以把这个管道想象成一个“水管”,网络进程接收到的字节流像水一样倒进这个“水管”,而“水管”的另外一端是渲染进程的 HTML 解析器,它会动态接收字节流,并将其解析为 DOM。

CSS 和 JS 会不会阻塞渲染

CSS

  • CSS 不会阻塞 DOM 的解析

  • CSS 阻塞页面渲染

因为浏览器是解析DOM生成DOM Tree,结合CSS生成的styleSheets,最终组成Layout Tree,再渲染页面,由此可见,在此过程中CSS完全无法影响DOM Tree,因而无需阻塞DOM解析,但是会阻塞页面渲染。

但是有一种情况会导致阻塞DOM解析,如下

<html lang="en">
  <head>
    <title>Title</title>
    <style>
      div {
      width: 100px;
      height: 100px;
      background: blue;
      }
    </style>
    <link rel="stylesheet" href="red.css"> <!-- 指定 div为红色 -->
    <script src="a.js"></script> <!-- 获取页面div标签并打印 -->
  </head>
  <body>
    <div></div>
  </body>
</html>

如果red.css三秒之后才返回,是浏览器会转圈圈三秒,但此过程中不会打印任何东西,之后呈现出一个红色的div,再打印null。这其实是JS文件在等待CSS文件下载完成之后再执行的JS文件,由于此时页面还没渲染完成,所以打印的是 null

这是因为如果脚本的内容是获取元素的样式,宽高等CSS控制的属性,浏览器是需要计算的,也就是依赖于CSS。浏览器也无法感知脚本内容到底是什么,为避免样式获取,因而只好等前面所有的样式下载完后,再执行JS。因而造成了之前例子的情况。

如果<script><link>同时在头部的话,<script>在上可能会更好;之所以是可能,是因为如果<link>的内容下载更快的话,是没影响的,但反过来的话,JS就要等待了,然而这些等待的时间是完全不必要的

如何优化

  • 对于大的 CSS 文件,可以通过媒体查询属性,将其拆分为多个不同用途的 CSS 文件,这样只有在特定的场景下才会加载特定的 CSS 文件
<link rel="stylesheet" type="text/css" href="foo.css" />
<link rel="stylesheet" type="text/css" href="foo.css" media="screen"/>
<link rel="stylesheet" type="text/css" href="foo.css" media="print" />

JS

  • JS 阻塞 DOM 解析

当HTML解析器解析到script标签时,这时候 HTML 解析器暂停工作,JavaScript 引擎介入,并执行 script 标签中的脚本。如果通过JavaScript 文件加载,则会先下载JavaScript 文件,然后执行,当执行完成后 HTML 解析器继续工作

如何优化

  • 使用 CDN 来加速 JavaScript 文件的加载
  • 压缩 JavaScript 文件的体积
  • 如果 JavaScript 文件中没有操作 DOM 相关代码,就可以将该 JavaScript 脚本设置为异步加载(asyncdefer

asyncdefer的区别如下

# 绿色 HTML 解析
# 灰色 HTML 解析暂停
# 紫色 下载 js
# 红色 执行 js
  • 无属性:在dom解析阶段开始下载,并且阻塞dom解析,下载完成之后再恢复dom解析。
  • defer:在dom解析阶段开始下载js,不阻塞dom解析,并在dom解析渲染完成之后再执行。需要在 DOMContentLoaded 事件之前执行。按顺序执行。

虽然理论上defer按加载顺序执行,但是也有失效的情况 👉这篇文章👈

  • async:在dom解析阶段开始下载js,不阻塞dom解析,在下载完成之后立即执行,如果dom正在解析则阻塞住。不保证执行顺序。

区别

推荐优先级依次是 asyncdefernormal

  • 如果依赖其他脚本和 DOM 结果,使用 defer
  • 如果与 DOM 和其他脚本依赖不强时,使用 async
  • 如果js文件很小且被 async script 依赖,使用正常模式的script且放在async script 前面

小结

Chrome 浏览器做了很多优化,其中一个主要的优化是 预解析操作(PreLoader)。当渲染引擎收到字节流之后,会开启一个预解析线程,用来分析 HTML 文件中包含的 JavaScript、CSS 等相关文件,解析到相关文件之后,预解析线程会提前下载这些文件。也就是说一边解析执行 js/css,一边去请求下一个(或下一批)资源

从图中可以看出来,在接收到 HTML 数据之后的预解析过程中,HTML 预解析器识别出来了有 CSS 文件和 JavaScript 文件需要下载,然后就同时发起这两个文件的下载请求,需要注意的是,这两个文件的下载过程是重叠的,所以下载时间按照最久的那个文件来算。

层爆炸

在说层爆炸之前先看下什么是隐式合成

隐式合成

上边提到,满足某些显性的特殊条件时,渲染层会被浏览器提升为合成层。除此之外,在浏览器的 Composite 阶段,还存在一种隐式合成,部分渲染层在一些特定场景下,会被默认提升为合成层。

  • 两个 absolute 定位的 div 在屏幕上交叠了,根据 z-index 的关系,其中一个 div 就会”盖在“了另外一个上边。
  • 这个时候,如果处于下方的 div 被加上了 CSS 属性:transform: translateZ(0),就会被浏览器提升为合成层。提升后的合成层位于 Document 上方,假如没有隐式合成,原本应该处于上方的 div 就依然还是跟 Document 共用一个 GraphicsLayer,层级反而降了,就出现了元素交叠关系错乱的问题。
  • 所以为了纠正错误的交叠顺序,浏览器必须让原本应该”盖在“它上边的渲染层也同时提升为合成层。

了解了隐式合成之后,接下来看什么是层爆炸

一些产生合成层的原因太过于隐蔽了,尤其是隐式合成。很容易就产生一些不在预期范围内的合成层,当这些不符合预期的合成层达到一定量级时,就会变成 层爆炸

层爆炸导致的问题:占用 GPU 和大量的内存资源,严重损耗页面性能

如何解决层爆炸

  • 不要盲目的提升合成层
  • 层压缩

层压缩

面对层爆炸这种问题,浏览器也有相应的应对策略,如果多个渲染层同一个合成层重叠时,这些渲染层会被压缩到一个 GraphicsLayer 中,以防止由于重叠原因导致可能出现的“层爆炸”。

  • 还是之前的模型,只不过这次不同的是,有四个 absolute 定位的 div 在屏幕内发生了交叠。此时处于最下方的 div 在加上了 CSS 属性 transform: translateZ(0) 后被浏览器提升为合成层,如果按照隐式合成的原理,盖在它上边的 div 会提升为一个新的合成层,第三个 div 又盖在了第二个上,自然也会被提升为合成层,第四个也同理。这样一来,岂不是就会产生四个合成层了?
  • 然而事实并不是这样的,浏览器的层压缩机制,会将隐式合成的多个渲染层压缩到同一个 GraphicsLayer 中进行渲染,也就是说,上方的三个 div 最终会处于同一个合成层中,这就是浏览器的层压缩。

当然了,浏览器的自动层压缩并不是万能的,有很多特定情况下,浏览器是无法进行层压缩的

position: fixed 引发的问题

Demo 如下,父元素设置了transform,子元素添加了相对窗口定位

<style>
    .content{
        transform: translateX(100px);
        width: 200px;
        height: 200px;
        background-color: #f00;
    }
    .elm {
        width: 50px;
        height: 50px;
        background-color: #00f;
        position: fixed;
        top: 0;
        left: 0;
    }
</style>

<div class="content">
    <div class="elm"></div>
</div>

即使子元素设置的是fixed,他还是会相对于父元素定位

image.png

查看 Layers 会发现子元素没有被提升成合成层,而是和document、父元素处于同一层中。这里有个疑问就是子元素明明设置了fixed,符合提升合成层的条件,为什么没有提升成合成层?

我的理解是子元素被层压缩了。如果提升成了合成层,子元素是就会相对于窗口定位,而不是相对于父元素,这样会导致层级关系有问题。为了保证层级关系,所以被压缩到了document层中。

requestAnimationFrame 回调执行时机

浏览器渲染原理中解释过显示器显示图像的原理解释

当通过渲染流水线通过GPU生成一张图片之后,会将图片存储到显卡的后缓冲区,一旦显卡把合成的图像写到后缓冲区,系统就会让后缓冲区和前缓冲区互换;此时显示器会从前缓冲区中获取最新图片。一般情况下显示器的刷新频率是 60HZ,也就是每秒更新 60 张图片。

也就是说渲染流水线需要在16.66667ms内就要生成一张图片。显卡每16.66667ms就要交换一次缓冲区;如果生成图片过久,显示器就会读取不到新的图片,并保持上一帧图片;从而给用户造成视觉上的卡顿。

当显示器将一帧画面绘制完成后,并在准备读取下一帧之前,显示器会发出一个垂直同步信号给浏览器,简称 VSync。浏览器接收到 VSync 信号之后,就可以准备绘制新的一帧了。在每一帧开始执行的时候,会执行requestAnimationFrame回调。

如下图:

image.png

相较于定时器

当通过setTimeOut执行动画时,其绘制时机是很难和 VSync 时钟保持一致的,从而可能会导致丢帧或者卡顿的情况出现。而requestAnimationFramecss动画都是和 VSync 时钟保持一致的。

requestIdleCallback 回调执行时机

接着上面的继续,对于60Hz的显示器来说,VSync 信号的发送频率是16.66667ms/次。如果渲染进程在16.66667ms内就完成整个操作,那么剩余的时间不会生成新的图片,而是可以在这段空闲时间内执行一些不那么紧急的任务,比如V8 的垃圾回收,或者执行window.requestIdleCallback()的回调任务

Chrome Performance常见名词解释

  • FP (First Paint)首次绘制;指渲染不同于导航前内容的时间点

  • FCP (First Contentful Paint)首次内容绘制;第一次渲染 DOM 的时间点,该内容可能是文本、图像、SVG 甚至 元素

  • LCP (Largest Contentful Paint)最大内容渲染;代表在 viewport 中最大的页面元素加载的时间. LCP的数据会通过PerformanceEntry对象记录, 每次出现更大的内容渲染, 则会产生一个新的PerformanceEntry对象.(2019年11月新增)

  • DCL (DomContentloaded)当 HTML 文档被完全加载和解析完成之后,DOMContentLoaded事件被触发,无需等待样式表、图片、视频和iframe加载完成

  • FMP(First Meaningful Paint)首次有效绘制;页面主要元素被渲染的时间点。比如视频网站中的视频

  • L (onLoad)当依赖的资源, 全部加载完毕之后才会触发 Lighthouse中的名词

  • TTI (Time to Interactive)可交互时间: 指标用于标记应用已进行视觉渲染并能可靠响应用户输入的时间点

  • SI (Speed Index)用于显示页面可见部分的显示速度, 单位是时间,下面是 Speed Index 的指标

Speed Index (in seconds)Color-coding
0–3.4Green (fast)
3.4–5.8Orange (moderate)
Over 5.8Red (slow)

其他

  • CLS累积布局偏移;标识用户交互期间的页面布局偏移

浏览器页面资源优先级

资源的优先级被分为5级,浏览器内核的5级分别是VeryHigh、High、Medium、Low、VeryLow,如下图

浏览器资源优先级顺序的计算过程如下,👉浏览器页面资源加载过程与优化👈

  • 第一步,根据资源的类型来设定默认优先级
    1. html、css、font这三种类型的资源优先级最高
    2. 然后是preload资源(通过<link rel=“preload">标签预加载)、script、xhr请求
    3. 接着是图片、语音、视频
    4. 最低的是 prefetch 预读取的资源
  • 第二步,根据一定的实际规则,对优先级进行调整。来确定出最终的加载优先级顺序。对于几个常见资源类型的调整规则如下:
    1. 对于XHR请求资源:将同步XHR请求的优先级调整为最高。(XHR请求可以分为同步请求和异步请求)
    2. 对于图片资源:会根据图片是否在可见视图之内来改变优先级。 图片资源的默认优先级为Low。现代浏览器为了提高用户首屏的体验,在渲染时会计算图片资源是否在首屏可见视图之内,在的话,会将这部分视口可见图片(Image in viewport)资源的优先级提升为High
    3. 对于脚本资源:浏览器会将根据脚本所处的位置和属性标签分为三类,分别设置优先级。
      • 首先,对于添加了defer/async属性标签的脚本的优先级会全部降为Low
      • 然后,对于没有添加该属性的脚本,根据该脚本在文档中的位置是在浏览器展示的第一帧之前还是之后,又可分为两类。在之前的(标记early**)它会被定为High优先级,在之后的(标记late**)会被设置为Medium优先级

关键请求链和优化

关键请求链(Critical-Request-Chains) 的概念。可视区域渲染完毕(首屏),并对于用户来说可用时,必须加载的资源请求队列,就叫做关键请求链。这样,我们可以通过关键请求链,来确定优先加载的资源以及加载顺序,以实现浏览器尽可能快地加载页面。

说白了就是首屏需要加载的图片、js、css等资源。关键请求链的报告如下图所示:图片来源:👉Web 性能优化:控制关键请求的优先级👈

image.png

如何查找页面的关键请求链

  1. 通过首屏快照获取关键image资源,如下图所示,我们通过首屏快照,来获取首屏所要加载的图片资源。(红框内) 图片来源:👉浏览器页面资源加载过程与优化👈
  1. 通过LightHouse插件获取关键请求链中的关键js和css资源

关键请求链最常见的一个例子是,某些样式表内部加载了在初始页面视口显示的字体或背景图像。

@font-face {
    font-family: d-din;
    src: url('//res.xxx.com/resources/D-DIN.otf');
    src: url('//res.xxx.com/resources/D-DIN.woff');
}

如何优化

  • 减少请求的数量
  • 使用压缩和最小化来减少资源的大小
  • 将非关键脚本标记为异步
  • 考虑将@font-face声明直接内联到 HTML 中
  • 使用压缩的字体格式,如 WOFF2 或 variable fonts(可变形字体)
  • 避免使用 CSS 背景图片或@import
  • 检查哪些请求必须在用户看到完整渲染的页面之前发出。使用 <link rel="preload" /> 对这些关键请求进行优先处理
  • 使用 link prefetching,优化可能在下一个导航中使用的资源
    资源预加载:<link rel="prefetch" href="test.css">
    DNS预解析:<link rel="dns-prefetch" href="//xxx.com">
    http预连接:<link rel="preconnect" href="//xxx.com">不光会解析 DNS,还会建立 TCP 握手连接和 TLS 协议
    页面预渲染:<link rel="prerender" href="//xxx.com"> 将会预先加载链接文档的所有资源
    
  • 利用LocalStorage,对部分请求的数据和结果进行缓存,省去发送http请求所消耗的时间,从而提高网页的响应速度。具体方式可以看这里👉浏览器页面资源加载过程与优化👈
  • 图片懒加载。可以通过第三方库和手写的方式;现在浏览器原生也支持了懒加载<img loading="lazy" />;需要确保设置图像的widthheight属性,以避免懒加载的图像在渲染时,页面重新布局
@font-face

每个人都目睹过字体出现,然后消失,改变了粗细,页面仿佛被震动了一样。这些移位现在会被累积布局移位(CLS)指标所测量。

可以用 font-display 来优化 Largest Contentful Paint(最大内容绘制)和 Cumulative Layout Shift(累积布局偏移)

所以大部分情况下都应该提高字体请求的优先级;

<link rel="preload" href="d-din.woff" as="font" crossorigin />

还可以用 CSS font-display进一步提高渲染速度。这个 CSS 属性允许你控制字体在请求和加载后如何展示。

有五个font-display选项供我们选择。推荐使用 swap 选项,它可以先立即呈现文本,然后在加载网络字体后立即替换。

body {
  font-family: Calibre, Helvetica, Arial;
}

加上后的提速效果可以看这里👉Web 性能优化:控制关键请求的优先级👈

最后

暂时就想到这么多,但是后期肯定还会添加,欢迎各位勘误指正!!!

感谢大佬们的文章🎉 🎉 🎉

持续更新中~~~