通过performance面板验证浏览器资源加载与渲染机制

前言

一个网页的核心由HTML、CSS和JavaScript组成,三者协同工作以呈现内容并实现交互。但浏览器如何解析这些资源?加载顺序如何影响用户体验?读完本本文你将彻底弄懂以下核心知识

  1. 为什么需要生成DOM树?
  2. CSS是否会阻塞HTML解析?是否阻塞页面渲染?
  3. JavaScript是否会阻塞HTML解析?
  4. JavaScript的异步加载机制如何影响解析过程?
  5. 如何通过performance面板验证以上观点

认识DOM

文档对象模型(Document Object Model,简称 DOM),是 W3C 组织推荐的处理可扩展置标语言的标准编程接口。DOM 把整个页面映射为一个多层的节点结构,HTML 或 XML 页面中的每个组成部分都是某种类型的节点,这些节点又包含着不同类型的数据。

W3C DOM 由以下三部分组成:

  • 核心 DOM - 针对任何结构化文档的标准模型
  • XML DOM - 针对 XML 文档的标准模型
  • HTML DOM - 针对 HTML 文档的标准模型

如果觉得这样介绍不够直观,可以看下这张图:

各个dom节点组合起来就形成了一个树状结构,所以我们通常称之为DOM树

为什么需要DOM树?

  • 结构化数据:将HTML标签(如<div><p>)和文本内容转化为节点对象,以树形结构表示标签的父子嵌套关系。
  • JavaScript动态操作的基础:这一过程解决了原生HTML文本的局限性,允许JavaScript通过属性与方法直接操作节点。
  • 渲染过程的核心输入:DOM树提供内容结构,CSSOM树提供样式规则,两者结合生成渲染树(Render Tree),决定页面元素的可见性与布局。
  • 安全性:DOM解析阶段会过滤恶意内容。

DOM 是浏览器对页面的内部表示,也是 Web 开发人员可以通过 JavaScript 交互的数据结构和 API。

解析HTML

网络线程获取HTML文件后,浏览器才会开始进行解析处理生成DOM树。

在这个过程中每个HTML标签都会被浏览器解析成文档对象,并且所有的文档对象最终都会被挂在document

比如:

并且为了提高解析效率,在解析之前,浏览器会启动一个预解析的线程,提前去下载文档中的外部CSS文件和外部JS文件。

解析CSS

在构建DOM的过程中,如果遇到link标签,当把它插入到DOM树上后,此时如果外部的CSS文件还没有下载完,主线程也不会停下来等待,因为**下载和解析CSS的工作是在预解析线程中进行的,**所以CSS并不会阻塞html的解析。

解析html的目的是为了生成DOM树,而解析CSS的目的同样是为了生成CSSOM树,两者都是为了转换成浏览器能够理解的结构,也可以方便javascript的访问。

我们可以通过document.styleSheets来查看它的结构:

CSSOM结构主要是为了给JavaScript提供操作样式表的能力 ,以及提供基础的样式信息

大体上来说,CSSOM是一个建立在web页面上的 CSS 样式的映射,它和DOM类似,但是只针对CSS而不是HTML,浏览器会将DOM和CSSOM结合生成渲染树。

CSS是否会阻塞渲染?

虽然CSS并不会阻塞html的解析,但由于渲染树的生成需要CSSOM的参与,所以CSS是会阻塞页面渲染的

真的原因是,如果浏览器在CSS检查之前展示了页面,那么每个页面都是没有样式的,等一会之后又突然有了样式,整个页面的体验就会很差。由于CSSOM被用作创建渲染树,那么如果不能高效的利用CSS会导致白屏时间的增加

解析javascript

在构建DOM的过程中,如果遇到script,在默认情况下主线程会停止对html的解析,转而等待 JS 文件下载好,并将全局代码解析执行完成后,才会继续解析html。这是因为 JS 代码的执行过程可能会修改当前的 DOM 树,所以 DOM 树的生成必须暂停。这就是 JS 会阻塞 HTML 解析的根本原因。

在html5中对script新增了两个属性可用于异步加载脚本,设置不同的属性对解析HTML文档也有很大的影响。

异步加载

这里我们要讨论的不仅仅是asyncdefer,还有type=module

默认情况

  • 在默认情况下,script 标签在请求和执行的时候都会阻塞文档解析

defer

延迟脚本执行 :带有defer属性的脚本,加载不会阻塞页面的解析和渲染过程,浏览器可以继续解析页面的其余部分,当整个文档完成解析后,在触发DOMContentLoaded事件之前执行这些脚本。

顺序执行:带有defer属性的脚本,尽管是异步加载的,但是它们之间会保持顺序执行。

async

非阻塞加载:带有async属性的脚本加载是异步的,不会阻塞HTML文档的解析,浏览器可以继续向下解析和渲染。不过,当脚本加载完成后,会立即执行脚本内的代码,此时如果HTML还没有解析完成,则会暂停对html的解析,从而阻塞页面渲染。但如果当脚本加载完准备执行之前,html已经解析完成,此时也不会阻塞页面渲染。

执行不可控 :带有async属性的脚本,执行是不可控的,因为无法确定脚本的下载速度与脚本内容的执行速度,如果存在多个script async时,他们之间的执行的顺序也是不可控的,完全取决于各自的下载速度,谁先下载完成就先执行谁。

module

非阻塞加载 :带有type="module"的脚本加载是异步的,这类标签视为ES6模块来处理,而ES6模块是设计为异步加载的,当浏览器遇到此类标签时,会开始异步下载改模块及其依赖项,不会暂停页面的解析和渲染工作,当HTML文档被解析完成后,会在触发DOMContentLoaded事件之前执行这些脚本。所以它的表现有点类似defer

模块化支持 :带有type="module"的脚本会自动分割成不同的模块,并且相互之间作用域是隔离的,浏览器会自动加载这些模块,无需手动管理依赖关系。

支持静态导入和动态导入 :可以使用import语句静态地导入其它模块,这些导入的模块加载时自动解析和执行。还可以使用import()函数动态地导入模块,根据需要在运行时加载模块,进一步控制模块的加载和执行时机。

module && async

表现类似async

通过performance验证

实验代码如下:

html 复制代码
<!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="./a.css" />
    <script>
        console.log('【readystatechange】', document.readyState)
        document.addEventListener('readystatechange', (e) => {
            console.log('【readystatechange】', document.readyState)
        })
        window.addEventListener('load', (e) => {
            console.log('【load】')
        })

        document.addEventListener('DOMContentLoaded', (e) => {
            console.log('【DOMContentLoaded】')
        })        
    </script>
    <script defer src="./a.js"></script>
    <script async src="./b.js"></script>
    <script src="./c.js"></script>  
</head>
<body>
    <div class="container"></div>
</body>
</html>

网络

页面中分别引入了一个css资源,三个JS资源以及一个內联脚本,我们可以看下各资源的加载情况

首先肯定是先加载html文件,而html中的cssjs资源会以他们在文档中的顺序依次进行请求加载,由于内联脚本无需再发起请求,所以在网络模块中也不会有它的存在。

注意看红线位置,这是html开始解析的时间点,从network模块上看,内部的css与js资源竟然在这之前就发起了请求,这也就验证了为了加速,浏览器的预加载扫描器会同时运行,如果在 html 中存在 <link><script>img 等标签,预加载扫描器会把这些请求传递给浏览器进程中的网络线程进行相关资源的下载。

从图中我们可以看到,a.cssc.js文件右上角都出现了红色标注,这是代表这两个文件都会阻塞页面的渲染

为了更清晰的了解各模块的加载解析与渲染之间的关系,我们可以查看下方的主线程模块

主线程

可以看到html的解析、css的解析、脚本的执行、页面渲染等都发生在主线程

parse html

在浏览器渲染引擎内部,有一个叫HTML 解析器(HTMLParser)的模块,它负责将HTML字节流转换为DOM结构。HTML Standard规范定义了浏览器渲染HTML为DOM的方法。

⚠️需要注意的是HTML解析器并不是等整个文档加载完成之后再解析的,而是网络进程加载了多少数据,HTML解析器就解析多少数据。

注意看,第一次解析html的范围是0-24,解析过程遇到了css资源,等css资源下载完成后会开始解析css(这个过程不会阻塞html的解析)

parse stylesheet

Evaluate script

再往后就遇到了内联脚本,这个时候会停下来解析执行JS(这才是导致html解析暂停的根本原因)

等脚本执行完后会继续解析html

注意range,跟上一次parse html刚好接上了

接着往后会依次遇到a、b、c三个脚本,由于c没有添加任何异步属性,所以c会率先开始执行(此时会阻塞html的解析)

执行完成后会继续解析html

等解析完成后会开始执行标记了defera.js(a.js在这之前就已加载完成)

最后标记了asyncb.js加载完成,会立即执行

相关推荐
GIS之路2 分钟前
GDAL 实现矢量裁剪
前端·python·信息可视化
是一个Bug6 分钟前
后端开发者视角的前端开发面试题清单(50道)
前端
Amumu121388 分钟前
React面向组件编程
开发语言·前端·javascript
学历真的很重要8 分钟前
LangChain V1.0 Context Engineering(上下文工程)详细指南
人工智能·后端·学习·语言模型·面试·职场和发展·langchain
持续升级打怪中29 分钟前
Vue3 中虚拟滚动与分页加载的实现原理与实践
前端·性能优化
GIS之路33 分钟前
GDAL 实现矢量合并
前端
hxjhnct35 分钟前
React useContext的缺陷
前端·react.js·前端框架
前端 贾公子1 小时前
从入门到实践:前端 Monorepo 工程化实战(4)
前端
菩提小狗1 小时前
Sqlmap双击运行脚本,双击直接打开。
前端·笔记·安全·web安全
前端工作日常1 小时前
我学习到的AG-UI的概念
前端