阿卡不拉阿卡不拉
Vue3
阿卡的博客
Vue3
阿卡的博客
  • Vue3

    • 快速入门

      • 搭建工程 👌
      • 模板语法
      • 响应式基础
      • 响应式常用 API
      • 计算属性
      • 类与样式绑定
      • 条件和列表渲染
      • 事件处理
      • 表单处理
      • 生命周期
      • 侦听器
      • 组件介绍
      • Props
      • 自定义事件
      • 组件v-model
      • 插槽
      • 前端路由介绍
      • KeepAlive内置组件
      • 状态管理库
      • 组件库介绍
    • 深入本质

      • 虚拟DOM本质
      • 模板的本质
      • 组件树和虚拟DOM树
      • 数据拦截的本质
      • 响应式数据的本质
      • 响应式的本质
      • 响应式和组件渲染
      • 实现响应式系统 1
      • 实现响应式系统 2
      • 图解EFFECT
      • 实现响应式系统 3
      • 手写computed
      • 手写watch
      • 指令的本质
      • 插槽的本质
      • v-model的本质
      • setup 语法标签
      • 组件生命周期
      • keepalive 生命周期
      • keepalive的本质
      • key的本质
    • 细节补充

      • 【Vue】属性透传
      • 【Vue】依赖注入
      • 【Vue】组合式函数 👌
      • 【Vue】自定义指令
      • 【Vue】插件
      • 【Vue】Transition
      • 【Vue】TransitionGroup
      • 【Vue】Teleport
      • 【Vue】异步组件
      • 【Vue】Suspense
      • 【Router】路由模式
      • 【Router】路由零碎知识
      • 【Router】路由匹配语法
      • 【Router】路由组件传参
      • 【Router】内置组件和函数
      • 【Router】导航守卫
      • 【Router】过渡特效
      • 【Router】滚动行为
      • 【Router】动态路由
      • 【State】通信方式总结
      • 【State】Pinia 自定义插件
      • 【场景】封装树形组件
      • 【场景】自定义 ref 实现防抖
      • 【场景】懒加载
      • 【场景】虚拟列表
      • 【场景】虚拟列表优化
      • 【第三方库】VueUse
      • 【第三方库】vuedragable
      • 【第三方库】vue-drag-resize
      • 【第三方库】vue-chartjs
      • 【第三方库】vuelidate
      • 【第三方库】vue3-lazyload
      • 【场景】Websocket 聊天室
      • 【Vite】✨ 认识 Vite👌
      • 【Vite】配置文件 👌
      • 【Vite】✨ 依赖预构建 👌
      • 【Vite】构建生产版本 👌
      • 【Vite】环境变量与模式
      • 【Vite】CLI
      • 【Vite】Vite 插件
  • 笔面试

    • HTML

      • HTML 面试题汇总
      • 文档声明
      • 语义化
      • W3C 标准组织
      • SEO
      • iframe
      • 微格式
      • 替换元素
      • 页面可见性
    • CSS

      • CSS 面试题汇总
      • CSS 单位总结
      • 居中方式总结
      • 隐藏元素方式总结
      • 浮动
      • 定位总结
      • BFC
      • CSS 属性计算过程
      • CSS 层叠继承规则总结
      • @import 指令
      • CSS3 calc 函数
      • CSS3 媒体查询
      • 过渡和动画事件
      • 渐进增强和优雅降级
      • CSS3 变形
      • 渐进式渲染
      • CSS 渲染性能优化
      • 层叠上下文
      • CSS3 遮罩
    • JavaScript

      • JavaScript 面试题汇总
      • ✨ let、var、const 的区别
      • JS中的数据类型
      • 包装类型
      • 数据类型的转换
      • 运算符
      • ✨ 原型链
      • ✨ this 指向
      • ✨ 垃圾回收与内存泄漏
      • ✨ 执行栈和执行上下文
      • ✨ 作用域和作用域链
      • ✨ 闭包
      • DOM 事件的注册和移除
      • DOM 事件的传播机制
      • 阻止事件默认行为
      • 递归
      • ✨ 属性描述符
      • class 和构造函数区别
      • 浮点数精度问题
      • 严格模式
      • ✨ 函数防抖和节流
      • ✨ WeakSet 和 WeakMap
      • ✨ 深浅拷贝
      • 函数柯里化
      • Node 事件循环
      • 尺寸和位置
    • 浏览器

      • 浏览器面试题汇总
      • ✨ 浏览器的渲染流程
      • ✨ 资源提示关键词
      • 浏览器的组成部分
      • IndexedDB
      • ✨ File API
      • ✨ 浏览器缓存
      • ✨ 浏览器跨标签页通信
      • Web Worker
    • 网络

      • 网络面试题汇总
      • 五层网络模型 👌
      • 常见请求方法 👌
      • ✨cookie👌
      • 面试题
      • 加密
      • ✨JWT👌
      • ✨ 同源策略及跨域问题 👌
      • 文件上传
      • ✨ 输入 url 地址之后
      • 文件下载
      • ✨ session
      • ✨ TCP
      • ✨ CSRF 攻击
      • ✨XSS 攻击 👌
      • ✨ 网络性能优化
      • 断点续传
      • 域名和 DNS
      • SSL、TLS、HTTPS 的关系
      • ✨ HTTP 各版本差异 👌
      • HTTP 缓存协议
      • ✨ WebSocket
    • 工程化

      • CMJ 和 ESM
      • npx
      • ESLint
    • Vue2

      • Vue 面试题汇总相关
      • 组件通信方式总结
      • 虚拟 DOM
      • v-model
      • 数据响应式原理
      • diff
      • 生命周期详解
      • computed
      • 过滤器
      • 作用域插槽
      • 过度和动画
      • 优化
      • keep-alive
      • 长列表优化
      • 其他 API
      • 模式和环境变量
      • 更多配置
      • 更多命令
      • 嵌套路由
      • 路由切换动画
    • Vue3

      • ✨ Vue3 整体变化 👌
      • ✨ Vue3 响应式变化 👌
      • ✨ nextTick 实现原理 👌
      • 两道代码题 👌
      • Vue 运行机制
      • 渲染器核心功能
      • 事件绑定与更新

✨ 资源提示关键词

在上一篇文章中,我们介绍了{% post_link '浏览器的渲染流程' '浏览器的渲染流程' %},这篇文章中,我们将重点聚焦在渲染阻塞上,来详细看一下渲染阻塞以及一些常见的解决方法。

本文主要包含以下内容:

  • 渲染阻塞回顾
  • defer 和 async
  • preload
  • prefetch
  • prerender
  • preconnect

渲染阻塞回顾

我们都知道,HTML 用于描述网页的整体结构。为了理解 HTML,浏览器必须将它转为自己能够理解的格式,也就是 DOM(文档对象模型)

浏览器引擎有一段特殊的代码,称为解析器,用于将数据从一种格式转换为另一种格式。

image-20211206161457653

浏览器一点一点地构建 DOM。一旦第一块代码进来,它就会开始解析 HTML,将节点添加到树结构中。

ezgif-2-2688553063

构建出来的 DOM 对象,实际上有 2 个作用:

  • HTML 文档的结构以对象的方式体现出来,形成我们常说的 DOM 树

  • 作为外界的接口供外界使用,例如 JavaScript(通过浏览器提供的 DOM API 来修改 DOM)。当我们调用诸如 document.getElementById 的方法时,返回的元素是一个 DOM 节点。每个 DOM 节点都有许多可以用来访问和更改它的函数,用户看到的内容也会相应地发生变化。

ezgif-2-01a1ded8c4

CSS 样式会被映射为 CSSOM( CSS 对象模型),它和 DOM 很相似,但是针对的是 CSS 而不是 HTML。

在构建 CSSOM 的时候,无法进行增量构建(不像构建 DOM 一样,解析到一个 DOM 节点就扔到 DOM 树结构里面),因为 CSS 规则是可以相互覆盖的,浏览器引擎需要经过复杂的计算才能弄清楚 CSS 代码如何应用于 DOM。

image-20211206161700033

当浏览器正在构建 DOM 时,如果它遇到 HTML 中的 <script>...</script> 标记,它必须立即执行它。如果脚本是外部的,则必须先下载脚本。

过去,为了执行脚本,必须暂停解析。解析会在 JavaScript 引擎执行完脚本中的代码后再次启动。

image-20211206161717368

为什么解析必须停止呢?

原因很简单,这是因为 Javascript 脚本可以改变 HTML 以及根据 HTML 生成的 DOM 树结构。例如,脚本可以通过使用 document.createElement( ) 来添加节点从而更改 DOM 结构。

image

这也是为什么我们建议将 script 标签写在 body 元素结束标签前面的原因。

<body>
    <!-- HTML Code -->
    <script>
        JS Code...
    </scirpt>
</body>

接下来我们回头来看一下 CSS 是否会阻塞渲染。

看上去 JavaScript 会阻止解析,是因为它可以修改文档。那么 CSS 不能修改文档,所以它似乎没有理由阻止解析,对吧?

但是,如果脚本中需要获取一些尚未解析的样式信息怎么办?在 JavaScript 中完全可以访问到 DOM 节点的某些样式,或者使用 JavaScript 直接访问 CSSOM。

image-20211206161801072

因此,CSS 的顺序可能间接导致解析被阻塞,因为脚本的执行依赖于 CSSOM。如果在脚本之前放置外部样式表,脚本的执行会被阻塞,直到 CSSOM 准备好,从而延迟 DOM 构建和解析。

当解析器到达一个脚本标签时,在 JavaScript 执行完成之前无法继续构建 DOM,然而如果这一段 JavaScript 中涉及到访问和使用 CSSOM,那么必须等待 CSS 文件被下载、解析并且 CSSOM 可用。如果 CSSOM 处于未可用状态,则会阻塞 JavaScript 的执行。

image-20211206161819188

(上图中 JavaScript 的执行被 CSS 构建 CSSOM 的过程阻塞了)

另外,虽然 CSS 不会阻塞 DOM 的构建,但是也会阻塞渲染。

还记得我们前面有讲过**要 DOM 树和 CSSOM 树都准备好,才会生成渲染树( Render Tree )**么,浏览器在拥有 DOM 和 CSSOM 之前是不会显示任何内容。

这是因为没有 CSS 的页面通常无法使用。如果浏览器向你展示了一个没有 CSS 的凌乱页面,那么片刻之后就会进入一个有样式的页面,不断变化的内容和突然的视觉变化会给用户带来混乱的用户体验。

2021-11-22 15.59.41

(这种糟糕的用户体验有一个名字,叫做“无样式内容闪现”,Flash of Unstyled Content,或者简称 FOUC )

为了解决这些问题,所以我们需要尽快的交付 CSS。

这也解释了为什么**“顶部样式,底部脚本”被称之为“最佳实践”**。

随着现代浏览器的普及,浏览器为我们提供了更多强大的武器(资源提示关键词),合理利用,方可大幅提高页面加载速度。

以下所有资源提示关键词的浏览器兼容情况可以通过网站Can I Use查看

defer 和 async

现代浏览器引入了 defer 和 async。

async 表示加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行(异步)。也就是说下载 JS 文件的时候不会阻塞 DOM 树的构建,但是执行该 JS 代码会阻塞 DOM 树的构建。

<script async src="script.js"></script>

defer 表示加载后续文档元素的过程将和 script.js 的加载并行进行(异步),但是 script.js 的执行要在所有元素解析完成之后,DOMContentLoaded 事件触发之前完成。也就是说,下载 JS 文件的时候不会阻塞 DOM 树的构建,然后等待 DOM 树构建完毕后再执行此 JS 文件。

<script defer src="myscript.js"></script>

具体加载瀑布图如下图所示:

image-20211208112125053

preload

preload 顾名思义就是一种预加载的方式,它通过声明向浏览器声明一个需要提前加载的资源,当资源真正被使用的时候立即执行,就无需等待网络的消耗。

<link rel="stylesheet" href="style2.css" />
<script src="main2.js"></script>
<!-- 使用preload之后,css和js都是用link标签 -->
<link rel="preload" href="style1.css" as="style" />
<link rel="preload" href="main1.js" as="script" />

在上面的代码中,会先加载 style1.css 和 main1.js 文件(但不会生效),在随后的页面渲染中,一旦需要使用它们,它们就会立即可用。

可以使用 as 来指定将要预加载的内容类型。

image-20211208112151152

preload 指令的一些优点如下:

  • 允许浏览器设置资源优先级,从而允许 Web 开发人员优化某些资源的交付。

  • 使浏览器能够确定资源类型,因此它可以判断将来是否可以重用相同的资源。

  • 浏览器可以通过引用 as 属性中定义的内容来确定请求是否符合内容安全策略。

  • 浏览器可以根据资源类型发送合适的 Accept 头(例如:image/webp )

prefetch

prefetch 是一种利用浏览器的空闲时间加载页面将来可能用到的资源的一种机制,通常可以用于加载非首页的其他页面所需要的资源,以便加快后续页面的首屏速度。

prefetch 加载的资源可以获取非当前页面所需要的资源,并且将其放入缓存至少 5 分钟(无论资源是否可以缓存)。并且,当页面跳转时,未完成的 prefetch 请求不会被中断;

它的用法跟 preload 是一样的:

<link rel="prefetch" href="/path/to/style.css" as="style" />

DNS prefetching

DNS prefetching 允许浏览器在用户浏览时在后台对页面执行 DNS 查找。这最大限度地减少了延迟,因为一旦用户单击链接就已经进行了 DNS 查找。

通过将 rel="dns-prefetch" 标记添加到链接属性,可以将 DNS prefetching 添加到特定 URL。建议在诸如 Web 字体、CDN 之类的东西上使用它。

<!-- Prefetch DNS for external assets -->
<link rel="dns-prefetch" href="//fonts.googleapis.com" />
<link rel="dns-prefetch" href="//www.google-analytics.com" />
<link rel="dns-prefetch" href="//cdn.domain.com" />

prerender

prerender 与 prefetch 非常相似,prerender 同样也是会收集用户接下来可能会用到的资源。

不同之处在于 prerender 实际上是在后台渲染整个页面。

<link rel="prerender" href="https://www.keycdn.com" />

preconnect

我们要讨论的最后一个资源提示是 preconnect。

preconnect 指令允许浏览器在 HTTP 请求实际发送到服务器之前设置早期连接。

我们知道,浏览器要建立一个连接,一般需要经过 DNS 查找,TCP 三次握手和 TLS 协商(如果是 https 的话),这些过程都是需要相当的耗时的。所以 preconnet,就是一项使浏览器能够预先建立一个连接,等真正需要加载资源的时候就能够直接请求了(其他页面加载的时候无需再次建立连接)。

image-20211208112216614

以下是为 CDN URL 启用 preconnect 的示例。

<link href="https://cdn.domain.com" rel="preconnect" crossorigin />

在上面的代码中,浏览器会进行以下步骤:

  • 解释 href 的属性值,判断是否是合法的 URL。如果是合法的 URL,然后继续判断 URL 的协议是否是 http 或者 https,如果不是合法的 URL,则结束处理。
  • 如果当前页面 host 不同于 href 属性中的 host(跨域),那么将不会带上 cookie,如果希望带上 cookie 等信息,可以加上 crossorign 属性。

最近更新:: 2025/7/16 12:57
Contributors: AK
Prev
✨ 浏览器的渲染流程
Next
浏览器的组成部分