浏览器渲染中的阻塞与 CRP

777 阅读4分钟

前言

JS、CSS、HTML 间的阻塞关系以及浏览器关键渲染路径 CRP 的相关概念和个人测试。参考文章已穿插标明。

一、JS 的阻塞

文章第 6 部分 JS 与关键渲染路径

1. JS 的加载和执行会阻塞 DOM 的解析

在构建 DOM 时,++HTML 解析器++若遇到了 JavaScript,那么它会暂停构建 DOM,将控制权移交给++JavaScript 引擎++,等 JavaScript 引擎运行完毕,浏览器再从中断的地方恢复 DOM 构建。

因为 JavaScript 可以修改 DOM。如果不阻塞,那么这边在构建 DOM,那边 JavaScript 在改 DOM,如何保障最终得到的 DOM 是否正确?而且在 JS 中前一秒获取到的 DOM 和后一秒获取到的 DOM 不一样是什么鬼?它会产生一系列问题,所以 JS 是阻塞的,它会阻塞 DOM 的构建流程,所以在 JS 中无法获取 JS 后面的元素,因为 DOM 还没构建到那。

(异步 JS 包括回调函数等异步代码暂时不会阻塞 DOM 解析。defer async 的 JS 文件的下载不会阻塞 DOM 解析)

2. 在引入 JS 后,CSS 的加载和执行会阻塞 JS 的执行,进而阻塞 DOM 解析

原本 DOM 和 CSSOM 的构建是互不影响,井水不犯河水,但是一旦引入了 JavaScript,CSSOM 也开始阻塞 DOM 的构建,只有 CSSOM 构建完毕后,DOM 再恢复 DOM 构建。

这是因为 JavaScript 不只是可以改 DOM,它还可以更改样式,也就是它可以更改 CSSOM。前面我们介绍,不完整的 CSSOM 是无法使用的,但 JavaScript 中想访问 CSSOM 并更改它,那么在执行 JavaScript 时,必须要能拿到完整的 CSSOM。所以就导致了一个现象,如果浏览器尚未完成 CSSOM 的下载和构建,而我们却想在此时运行脚本,那么浏览器将延迟脚本执行和 DOM 构建,直至其完成 CSSOM 的下载和构建。

也就是说,在这种情况下,浏览器会先下载和构建 CSSOM,然后再执行 JavaScript,最后在继续构建 DOM。

这时,DOM 和 CSSOM 不能再同时解析,DOM 需要等 JS 执行完成后再解析。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <link
      rel="stylesheet"
      href="https://unpkg.com/element-ui/lib/theme-chalk/index.css"
    />
  </head>
  <body>
    aa
    <script>
      console.log(1);
    </script>
  </body>
</html>

无 script: CSS 的加载和解析不阻塞 DOM 解析。

有 script: CSS 的加载和解析完成 -> JS 执行 -> DOM 解析

3. 加载总是并行的

第 3 部分 关键渲染路径与阻塞渲染

二、Critical Rendering Path

第 2 部分 渲染过程

渲染过程

1. 生成 DOM 树:DOM 树的构建是深度遍历

DOM 和 CSSOM 的构建步骤:Bytes -> Characters -> Tokens -> Nodes -> DOM / CSSOM

2. 生成 Render 树

3. 布局

计算出在屏幕上的准确位置

4. 绘制

显示在页面

5. 合并 composite

重绘和回流

重绘和回流的时机

有时不是立即触发,浏览器会积攒一批

有时立即触发,比如 resize 窗口、改变页面默认字体,都会立即触发 reflow

引发重绘和回流的事件

何时发生回流重绘

三、浏览器引擎与线程

第 1 部分 浏览器主要组成与浏览器线程

进程是 CPU 资源分配的最小单位(是能拥有资源和独立运行的最小单位)。 线程是 CPU 调度的最小单位(是建立在进程基础上的一次程序运行单位)。

Chrome 中一个标签页就是一个进程。这样一个页面崩溃不会影响到其它页面。

  1. GUI 渲染线程:负责布局和重绘(也有说,dom 解析、css 解析都是 GUI)
  2. JS 引擎线程: 是单线程。JS 引擎和 GUI 引擎互斥。
  3. 定时器触发线程: 浏览器定时计数器不由 JS 引擎计数,通过单独引擎完成。
  4. 事件触发线程: 当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待 JS 引擎的处理。
  5. 异步 http 请求线程: 在 XMLHttpRequest 在连接后是通过浏览器新开一个线程请求,将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到 JS 引擎的处理队列中等待处理。