渲染阻塞回顾
-
HTML 用于描述网页的整体结构;为了理解 HTML 浏览器必须将它转为自己能够理解的格式,也就是 DOM(文档对象模型),浏览器引擎有一段特殊的代码,称为解析器,用于将数据从一种格式转换为另一种格式;
-
浏览器一点一点地构建 DOM;一旦第一块代码进来,它就会开始解析 HTML ,将节点添加到树结构中;
-
构建出来的 DOM 对象,实际上有 2 个作用:
- HTML 文档的结构以对象的方式体现出来,形成我们常说的 DOM 树;
- 作为外界的接口供外界使用,例如 JavaScript;当我们调用诸如 document.getElementById 的方法时,返回的元素是一个 DOM 节点;每个 DOM 节点都有许多可以用来访问和更改它的函数,用户看到的内容也会相应地发生变化;
-
CSS 样式会被映射为 CSSOM( CSS 对象模型),它和 DOM 很相似,但是针对的是 CSS 而不是 HTML;在构建 CSSOM 的时候,无法进行增量构建(不像构建 DOM 一样,解析到一个 DOM 节点就扔到 DOM 树结构里面),因为 CSS 规则是可以相互覆盖的,浏览器引擎需要经过复杂的计算才能弄清楚 CSS 代码如何应用于 DOM;
-
当浏览器正在构建 DOM 时,如果它遇到 HTML 中的
<script>...</script>
标记,它必须立即执行它;如果脚本是外部的,则必须先下载脚本;过去,为了执行脚本,必须暂停解析;解析会在 JavaScript 引擎执行完脚本中的代码后再次启动;
-
为什么解析必须停止呢?原因很简单,这是因为 Javascript 脚本可以改变 HTML 以及根据 HTML 生成的 DOM 树结构。例如,脚本可以通过使用 document.createElement( ) 来添加节点从而更改 DOM 结构;
-
这也是为什么我们建议将 script 标签写在 body 元素结束标签前面的原因;
<body> <!-- HTML Code --> <script> JS Code... </scirpt> </body>
-
-
接下来我们回头来看一下 CSS 是否会阻塞渲染;看上去 JavaScript 会阻止解析,是因为它可以修改文档;
-
那么 CSS 不能修改文档,所以它似乎没有理由阻止解析,对吧?但是,如果脚本中需要获取一些尚未解析的样式信息怎么办?在 JavaScript 中完全可以访问到 DOM 节点的某些样式,或者使用 JavaScript 直接访问 CSSOM;
-
因此,CSS 可能会根据文档中外部样式表和脚本的顺序阻止解析;如果在文档中的脚本之前放置了外部样式表,则 DOM 和 CSSOM 对象的构建可能会相互干扰;
-
当解析器到达一个脚本标签时,在 JavaScript 执行完成之前无法继续构建 DOM ,然而如果这一段 JavaScript 中涉及到访问和使用 CSSOM ,那么必须等待 CSS 文件被下载、解析并且 CSSOM 可用;如果 CSSOM 处于未可用状态,则会阻塞 JavaScript 的执行;
-
-
另外,虽然 CSS 不会阻塞 DOM 的构建,但是也会阻塞渲染;还记得前面有讲过要 DOM 树和 CSSOM 树都准备好,才会生成渲染树( Render Tree )么,浏览器在拥有 DOM 和 CSSOM 之前是不会显示任何内容;
-
这是因为没有 CSS 的页面通常无法使用;如果浏览器向你展示了一个没有 CSS 的凌乱页面,那么片刻之后就会进入一个有样式的页面,不断变化的内容和突然的视觉变化会给用户带来混乱的用户体验;
-
这种糟糕的用户体验有一个名字,叫做
无样式内容闪现
Flash of Unstyled Content,或者简称 FOUC; -
为了解决这些问题,所以我们需要尽快的交付 CSS ,这也解释了为什么
顶部样式,底部脚本
被称之为最佳实践
; -
随着现代浏览器的普及,浏览器为我们提供了更多强大的武器(资源提示关键词),合理利用,方可大幅提高页面加载速度;
-
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>
-
具体加载瀑布图如下图所示:
preload
-
preload 顾名思义就是一种 预加载 的方式,它通过声明向浏览器声明一个需要提前加载 当前页的资源,当资源真正被使用的时候立即执行,就无需等待网络的消耗;
<link rel="stylesheet" href="style2.css"> <script src="main2.js"></script> <link rel="preload" href="style1.css" as="style"> <link rel="preload" href="main1.js" as="script">
-
在上面的代码中,会先加载 style1.css 和 main1.js 文件(但不会生效),在随后的页面渲染中,一旦需要使用它们,它们就会立即可用;可以使用 as 来指定将要预加载的内容类型;
-
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 查找;这最大限度地减少了延迟,因为一旦用户单击链接就已经进行了 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 指令允许浏览器在 HTTP 请求实际发送到服务器之前设置早期连接;
-
浏览器要建立一个连接,一般需要经过 DNS 查找,TCP 三次握手和 TLS 协商(如果是 https 的话),这些过程都是需要相当的耗时的;所以 preconnet,就是一项使浏览器能够预先建立一个连接,等真正需要加载资源的时候就能够直接请求了;
-
以下是为 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 属性;
浏览器🧑💻 浏览器的渲染流程
上一篇