Day09 X Resource Hint & Non-Blocking Script Tag

经过昨天的内容,读者们应该对於网页的渲染流程有大致的理解了。

我们再小小复习一下,大致上网页的渲染流程为:

  • 读取 HTML 後生成 DOM Tree
  • 读取 HTML 中的 CSS Link Tag 生成 CSSOM Tree
  • DOM Tree 与 CSSOM Tree 共同生成 Render Tree
  • 根据 Render Tree 生成 Layout Tree,负责各元素大小与位置的计算
  • 最後 Paint 画面

从收到 HTML、CSS 和 JavaScript,再对程序码进行必需的处理,到最後转变为显示像素的过程中还有许多中间步骤。将效能最佳化其实就是了解这些步骤中所有的活动,再经过最佳化,这就是所谓的关键渲染路径 Critical Render Path

Non-Blocking Script

当然,现今的 web app 不太可能只靠 HTML 跟 CSS 就完成,还是得靠 JavaScript 来修改网页的内容、样式、与使用者互动的行为。JavaScript 可以查询及修改 DOM 和 CSSOM,在 CSSOM 执行完毕後,JavaScript 才会执行 。这边给一个小 tip:如果可以的话,CSS file 尽快引入,JS 在 CSS 後引入,因为 JS 的执行会导致网页载入的暂停。

一般来说我们会在 HTML 中引入 script tag 来载入 JavaScript

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

刚刚也提过 JS 的执行会导致网页载入的暂停,当解析 HTML 时遇到 script tag,会立即载入指定的 JavaScript 并在载入後立即执行它,执行完後才会继续解析 HTML 的工作。

不过其实 script tag 还有 async 跟 defer 这两种方式,也就是今天要介绍的 Non-Blocking Script。

async script

async 会非同步去请求外部脚本,回应後停止解析 HTML,马上执行脚本内容,如果有多个 script 则没办法保证执行的先後顺序。由於脚本执行时没办法确保 DOM 已经全部渲染(下图中 JS exec 时 HTML 还没完全 Parse 完),因此常用於载入第三方函式库等不需要动到 DOM 结构的状况。

async 最经典的例子大概是载入 Google Analytics 等网站分析工具了,因为它的载入并不是很紧急,也没有与其他模组互相依赖,不需要注意 script 执行顺序的问题,当然,它也不会去动到 DOM 结构。

还有一种方法是透过 JavaScript 动态塞入 script tag,这种动态塞入的 script tag 预设就会是以 async 的方式载入,不过可以透过设定属性来将非同步载入关闭

const script = document.createElement('script');
script.src = "/itironman/kylemo.js";
document.body.append(script);

// 关闭 async
script.async = fasle;

defer script

defer 也会非同步请求外部脚本,但是载入的脚本会等待浏览器解析完 HTML 才执行(实际上的执行时间,会在 DOMContentLoaded 执行之前),类似於把 JS 放在页尾的情况。与 async 不同,defer 会保证执行的先後顺序是依照 script tag 的顺序(由上至下)。由於非同步载入、不打断渲染流程及确保执行顺序的特色,基本上如果是不是那麽紧急的 script,都可以加上 defer,当然 script tag 摆放顺序自己需要留意一下。


在适当的时机选用不同的载入方式,是有机会提升网页的效能的,对於 Critical Render Path 或资源载入方式有兴趣的读者,可以更进一步阅读 Google Developer 的文章或 MDN script tag 的 document

Resource Hint

刚刚介绍的 Non-Blocking Script 原理类似於「由我们告诉浏览器」脚本的载入与执行时机,接下来要介绍的 Resource Hint 也有异曲同工之妙,可以「由我们提供一些资讯给浏览器」,让浏览器依照我们的提示预先下载一些网页上会用到重要资源,以达到效能优化的效果。

Resource Hint 不是什麽开源的第三方套件,而是浏览器本身提供的效能优化指令。虽然现在的浏览器已经很强大了,不过碍於一些流程限制,浏览器有些时候也许没办法得到想要的资讯,这时候浏览器希望可以透过我们来亲自告诉它它想要的资讯,让它更方便做一些优化,这就是 resource hint 这些指令存在的意义。

还记得 Day6 图片最佳化有提到 srcset 与 sizes 这两个属性吗?它们「广义」来说其实也属於 Resource Hint(根据 caniuse 分类)。因为渲染流程的限制,浏览器在解析 HTML 时因为还没有读取 CSS,没有办法得知图片的大小,因此希望我们可以透过属性提前告知它,让它能依据我们给予的提示做一些优化。

用「严格」一点的定义来看待的话, Resource Hint 指的是带有 rel attribute 的 link tag,也就是今天要介绍的五种方式。

今天主要要介绍 5 种 resource hint:

  • preload
  • prefetch
  • preconnect
  • dns-prefetch
  • prerender

(其实严格来说 preload 不算是 Resource Hint,因为它有自己独立的 W3C spec,是一个 deprecated 的 subresource prefetching feature 的替代版本,不过因为概念雷同,这边我还是将它列进去,待会会详细说明它与其他 Resource Hint 的差别。)

虽然光看名称应该会让人觉得一头雾水,不过其实它们都有一个共同的目标,或者说共同的效能优化方式

对不久的将来会用到的资源预先处理,这里的处理有可能是载入资源,或是建立连线,因此在真的要使用到该资源时可以省去不少时间。

而我们今天也有一个共同的目标,就是一起好好了解它们 ?,在开始之前,我想先为各位建立一个 mindset:Resource Hint 虽然强大,但是使用它们其实都是在增加效能与浪费网路资源这两者之间做取舍。

举一个生活化的例子,今天你跟朋友约好要去野餐,你预先想到了你们可能会需要喝饮料解解渴,於是你在出发前特别到全联买了两大袋的饮料,朋友看到你都说你太聪明了!居然想到要先买好饮料。结果事实是大家根本就不想喝饮料,於是乎野餐结束後你还是提着那两大袋饮料回家了,心里想着:「哎!一整天都搬这两袋,超重的,没喝完也不好意思跟其他人收钱,今天亏大了!」

错用 Resource Hint 也是一样的道理,预先载入了一堆根本不会用到的资源,对效能只是负担而已,因此 Resource Hint 并不是万灵丹,越强大的东西越需要谨慎思考如何使用,何时要使用。

(既然是 Browser 提供的功能,免不了会有浏览器支援度的问题,读者可以参考这里查看个浏览器对各个功能的支援度。)

preload & prefetch

preload 与 prefetch 是两个较常被搞混的技巧,两者的作用都是在提早取得将来会用到的资源,然而两者的差别在於:

Preload 用来取得「当前页面」的重要资源,例如影片点击播放前的缩图(跑不出缩图的影片,不知道影片主要在讲什麽,你敢点吗XDD)。可以想像成告诉浏览器:「越快帮我下载这些资源越好!」

Prefetch 告诉浏览器「这些资源我待会会用到,先帮我下载吧!」不过与 preload 不同的是 prefetch 抓取的资源不限於当前页面使用,也就是可以跨越 navigation,例如你很确定使用者会点击下一页,就可以使用 prefetch 预先抓取下一页的资源,至於什麽时候要下载,则交由浏览器自行决定。

至於刚刚提到严格来说 preload 不算是 Resource Hint,除了它拥有自己的 spec 以外,它与其他四者最大的不同在於另外四个 Resource Hint 是真的「Hint」,它们建议浏览器可以先去载入哪些资源或是做哪些事,对浏览器而言,这些 hint 的 priority 是比较低的,当浏览器有 idle time 再去做就好。而 preload 比较像是强制的告诉浏览器:「我现在就要这个!马上帮我下载,不然我哭给你看!」逼不得已下浏览器得把 preload 要的资源视为 high priority,相较於其他 "hint",preload 更像是强制性的 "command"。


(不跟我玩,我就哭给你看!)

as Attribute

浏览器对於资源的载入顺序是有规则的,是以档案类型来决定下载的优先顺序,以 chrome 举例来说

High priority : style | font | XHR (sync) 

Medium priority : 位於可视区域的图片 | Preload without as attribute | XHR (async) 

Low priority : favicon、script async | defer | block、不在可视区域的图片、媒体档、SVG 等

preload 与 prefetch 也是以 as 属性来分辨档案类型

<link rel="preload" as="font" type="font/woff2" href="myfont.woff2">

可以看到使用 preload 後浏览器将资源的载入优先顺序提高了。


上面这张表为 chrome 资源载入顺序对照表。
Reference

从浏览器 devtool 的 network tab 也可以看到每个资源的 priority。

crossorigin Attribute

其实 preload、prefetch、preconnect 都有一个 crossorigin 的 attribute,例如

<link rel="preload" as="font" crossorigin type="font/woff2" href="myfont.woff2">

如果都不带的话,代表不会去挡跨来源的资源请求,如果像上面的范例只带 crossorigin,则预设会是 crossorigin=anonymous,只有 same-origin 的请求才能通过。因为跟效能比较没有关系,就贴个 MDN 文件让有需要的读者自行研究罗。


preconnect

preconnect 相当於告诉浏览器:「这个网页将会在不久的将来下载某个 domain 的资源,请先帮我建立好连线。」

要理解 preconnect 能够达成的事,得了解浏览器在实际传输资源前,实际上经过哪些步骤:

  • 向 DNS 请求解析网域名,拿到 IP 地址 (DNS Resolution)
  • TCP Handshake
  • (HTTPS connection) SSL Negotiation
  • 建立连线完成,等待拿到资料的第一个byte

上面的四个步骤中,每一步都会需要一个 RTT (Round Trip Time) 的来回时间。所以在实际传输资料之前,已经花了3个 RTT 的时间。如果在网路状况很差的状况下,会让获取资源的速度大大降低。

利用 preconnect 提早建立好与特定 domain 之间的连线,省去了一次完整的 (DNS Lookup + TCP Handshake + SSL Negotiation) ,共三个 Round Trip Time 的时间。

Preconnect Use Cases :

  • 通常只会对确定短时间内就会用到的 domain 做 preconnect,因为如果 10 秒内没有使用的话浏览器会自动把连线 close 掉。
  • CDN:如果网站中有很多资源要从 CDN 拿取,可以 preconnect CDN 的域名,这在不能预先知道有哪些资源要抓取的情况,是蛮适合的 use case。各位还记得在 Day6 图片优化的章节提到的 Image CDN 吗?它也是个蛮适合使用 preconnect 的情境喔!
  • Streaming 串流媒体 (待会看下方 lite-youtube-embed 的例子)
  • Dynamic URL request

dns-prefetch

跟 preconnect 类似,差别在於只提示浏览器预先处理第一步 DNS lookup 而已。也就是说

dns-prefetch= DNS look up

preconnect = DNS look up + TCP Handshake +  SSL Negotiation

让我猜猜你在想什麽,「看起来 preconnect 做了比较多优化,有点不太清楚 dns-prefetch 存在的意义跟使用的时机?」

其实基本上适合使用 preconnect 的情境都可以套用在 dns-prefetch 上,毕竟 dns-predetch 算是 preconnect 的子集。还记得一开始建立的 mindset 吗?因为 preconnect 做了更多的事,相较之下它也会耗费更多的 bandwidth,再加上前面提过的浏览器只会保持 preconnect 的 connection 10 秒,超过 10 秒都没有跟连线目标发送请求,浏览器会自动关闭连线,那麽 preconnect 就等於是白做了,还浪费了一些网路资源。

Only For Cross-Origin Domains

根据 MDN doc,只针对 cross-origin domain 做 dns-prefetch 是一个 best practice,因为同源的 IP Address 早就被解析过了。

preconnect Pair With dns-prefetch

<link rel="preconnect" href="https://fonts.googleapis.com/" crossorigin>
<link rel="dns-prefetch" href="https://fonts.googleapis.com/">

根据 MDN doc,如果确定资源适合做 preconnect,建议一起使用 preconnect 与 dns-prefetch,原因在於浏览器支援度,preconnet 的支援度比 dns-prefetch 还差,多亏 HTML fault-tolerant 的特色,如果浏览器看到不支援的 hint,会忽略它而不会报错,将 preconnect 与 dns-prefetch 一起使用的状况下可以确保就算浏览器没有支援 preconnect,也能最小限度先做 DNS Resolution。

最後总结一下 preconnect 与 dns-prefetch 的使用时机

如果页面需要与许多第三方网域建立 connection,将它们都做 preconnect 反而会适得其反。 preconnect hint 最好仅用於最关键的 connection。对於其他资源,只需使用 即可节省第一步 DNS Resolution 的时间。


prerender

prerender 与 prefetch 都是针对非当前页面的资源载入,不过 prerender 比 prefetch 更进一步。不仅仅会下载对应的资源,还会对资源进行解析。解析过程中,如果需要其他的资源,还会直接下载或执行这些资源,基本上就是尽可能预先渲染下个页面,这样一来当用户在从当前页面跳转到目标页面时,浏览器可以快速的响应。适合用在用户很高机率会转到另一个页面的状况下使用。

举个例子

<link rel="prerender" href="next-page.html">

浏览器会先去抓取 next-page.html,除了抓取之外,还会直接解析这个 HTML 档,如果这个 HTML 有引入其他 script,浏览器会直接执行这些 script,就像是预先渲染完这个页面一样。

既然 prerender 不只抓取资源,甚至会去执行它,在使用上就得格外小心。使用 prerender 的资源应该要是非常确定使用者在不久後一定会存取的页面,不然反而浪费了更多的 Network Bandwidth。

不过 prerender 的浏览器支援度相比 prefetch 就没有那麽好了,至少目前为止是这样,因此以相容性的观点来看,prefetch 目前应该会是比 prerender 更好的选择。


Resource Hint Case Demo - lite-youtube-embed

今天介绍了那麽多 Resource Hint 的技巧,不实际 demo 一下证明对效能真的有帮助好像有点说不过去,让我们直接来看个例子吧!

lite-youtube-embed 是一个号称渲染速度比原生 iframe 快 224 倍的 YouTube 影片播放元件,它能达成这样的效能提升其实做的事并不复杂,主要有两件事:

  • 看过 YouTube 的人都知道影片会有一个预览缩图(thumbnail),以使用者的角度来看,如果一个影片的预览图一直跑不出来,你应该也不敢乱点进去看那个影片吧?这个套件利用 preload resource hint,要浏览器尽快下载 YouTube 影片的 thumbnail,使得使用者可以尽早看到预览图,提升使用者体验。

  • 当使用者鼠标移到元件范围时(hover),对 YouTube domain 进行 preconnect,当使用者真的点下播放键时才真正载入 iframe,不过因为有对 youtube domain 先做 preconnect,因此省去 3 个 Round Trip Time 的时间,因此可以更快速开始播放影片。

// pseudo code

// preload 影片的 thumbnail 缩图
<link rel='preload' href={thumbnail URL} as='image' />

{当滑鼠 hover 到影片上 && (
  <>
    {/* The iframe document and most of its subresources come right off youtube.com */}
    <link rel='preconnect' href={youtube domain} />
    {/* The botguard script is fetched off from google.com */}
    <link rel='preconnect' href='https://www.google.com' />
  </>
)}

啊...说好要 demo,怎麽直接放别人的成果咧?

其实我看到这个套件的当下,觉得这些优化的想法太棒了,简单又高效,所以试着用相同的概念实作了一个 React Component 版本的 lite-youtube-embed,叫做 react-lite-yt-embed,有兴趣的读者可以随意看看,除了原作者使用的 resource hint 以外,我还有做一些额外优化例如支援 WebP 与 lazy load,如果觉得对你有帮助,可以不吝啬的给我一颗 star ?

从这张 gif 可以看到除了影片缩图是透过 preload 载入以外,当滑鼠 hover 到图片上时还会先去做 preconnect,当使用者点下播放键後,因为已经提前 preconnect,可以减少一些 round trip time,加快影片载入速度。

有兴趣的读者也可以到 codesandbox 范例玩玩。

本日小结

随着浏览器不断进化,它也提供越来越多功能给我们使用,不管是 Non-Blocking Script 还是 Resource Hint,只要使用时机恰当,都很有可能靠浏览器帮我们完成效能的优化,不过它们也有可能是一把双面刃,如果不当使用,则只是在浪费网路资源,反而造成效能的耗损。

使用 Resource Hint 也很容易让 code 变的难以维护(想想如果都单纯把 hint 加到 HTML 里,万一资源有变动,要改动真的十分麻烦),比较合理的方式可能是使用 JavaScript 动态产生这些 Resource Hint,不只可以把相关的 hint 写在同一个 file,维护上也变得更加容易。(额外补充,Resource Hint 都是 body-ok 的 link,要放在 HTML 的 body 也是可以 work 的)。

最後也真心感谢还继续支持着系列文的小夥伴,我知道有几天的篇幅还蛮长的(未来的天数应该也是差不多这个长度 XDD),可能会让阅读的人稍微辛苦一点,但这是我给自己的一个小目标,想在系列文把想分享的知识毫无保留的写出来,相信也真的可以让读者有所收获,我们明天见罗~

References

https://shubo.io/preload-prefetch-preconnect/
https://cythilya.github.io/2018/07/31/preload-vs-prefetch/
https://medium.com/reloading/preload-prefetch-and-priorities-in-chrome-776165961bbf
https://www.smashingmagazine.com/2016/02/preload-what-is-it-good-for/


<<:  [Day24] 供新手参考的几个可以实作的方向

>>:  Day 12【连动 MetaMask - Backend & Init】277353

Day 8 Self-attention(二) 如何算出input彼此是相关的?

前言 昨天讲到为什麽要使用self-attention,今天稍微来介绍一下self-attentio...

【Day6】窗涵式,n_fft ,hop_length 到底什麽意思啊?

回填之前的坑 在往 Vocoder 迈进之前,我们先回顾一下之前我们在做 melspectrogra...

在 Windows 10 上安装 Visual Studio Code EP3

前言 写程序,设定好 IDE,可以增加自己的效率,今天来纪录一下安装 Visual Studio C...

Day 13 优化你广告活动的小撇步

广告优化除了要随时搭配工具监控数字之外,当然每个产品或是活动看的指标也都不尽相同,会给个建议: 每个...

Day14 - BST ( Find & Insert )

大家好,我是长风青云。今天是铁人赛第十四天。恭喜你们听到我带点磁性的声音(?) 我要滚去休息了。 ...