直接嵌入 iframe 對網頁效能有害
一般來說大家習慣在網頁上嵌入 YouTube 或是 third-party resource 像是 Twitter 的貼文,以至於我們寫範例常用的 CodeSandbox,大部分都是以 iframe 嵌入網頁的方式來使用,原因不外乎簡單好用又方便。但你是否曾經想過這些 iframe 事實上相當吃資源,且讓你的使用者數據變成數位廣告投遞的來源的風險。
embeded iframe 有多大 #
我把下列 embeded iframe 頁面單獨拉出來用 chrome 測出來的 network 流量與效能數據。
- Youtube https://www.youtube.com/embed/cowtgmZuai0
- Tweet https://platform.twitter.com/embed/Tweet.html?id=1488881026994778116
- CodeSandbox https://codesandbox.io/embed/pj127rwpvq
所得到的數據如下:
js size | script runtime | |
---|---|---|
youtube-embed | 762 KB | 530 ms |
tweet-embed | 401 KB | 614 ms |
codesandbox-embed | 1.9 MB | 1602 ms |
但對於大部分的使用者來說,也許他們是用手機來到你的網站,並且只想快速瀏覽資訊,並不想看影片或是你嵌入的 iframe 有更深入的互動,你的 iframe 只是拖累你的網站速度,並不能讓大部分的使用者受益。
iframe 的 RWD 排版問題 #
雖然 youtube 有固定長寬比可以用 aspect-ratio
指定,由於 iframe 是另外一個網頁,如果是有文字內容的 iframe,高度在 RWD 時常常會因為文字隨著不同寬度的產生不同的換行而變化,我們一開始計算出 iframe 的長寬比就會有困難,無法指定長寬比就會使得 iframe 的載入會造成畫面的突跳,Web Vitals 的 CLS 分數就不會太好看。當然你也可以考慮固定 iframe 高度,overflow 就用 scrolling 的做法,但這在 mobile 垂直方向如果出現兩個 scrolling bar 容易會造成互相干擾。
embeded YoutTube iframe 解決方案 #
其實這個解決方案 web 一直都存在已久,就是 link。對於想要深入探索這個內容的人我們可以提供一個連結點擊,讓他可以到另外一個頁面去互動,而想停留在原本頁面的人看其他資訊的人,他們可以繼續往下走,而 iframe 其實就是一個我想停留在原頁面,同時也可以呈現其他頁面的解決方案,但其實我們可以透過 js 保留雙方的優點,我們一開始不直接嵌入 iframe,而是提供一個類似的元素可以讓使用者預覽 iframe 的內容,等到使用者想進一步了解在點擊的時候置換成真實的 iframe。這種靜態元素提供 iframe preview 但並無實際功能,Google 稱之為 Facade 。這樣的概念其實不需要太過複雜的前端框架,簡單的 js 跟 html 就可以實現。以 YouTube 為例來實現 facade 如下:
首先在 html 提供下面的 element,即可擁有影片縮圖預覽以及播放按鈕。
<div data-src="https://www.youtube.com/embed/cowtgmZuai0">
<img src="https://i.ytimg.com/vi/cowtgmZuai0/hqdefault.jpg" loading="lazy" />
<svg height="48" width="68" version="1.1" viewBox="0 0 68 48">
<path
d="M66.52,7.74c-0.78-2.93-2.49-5.41-5.42-6.19C55.79,.13,34,0,34,0S12.21,.13,6.9,1.55 C3.97,2.33,2.27,4.81,1.48,7.74C0.06,13.05,0,24,0,24s0.06,10.95,1.48,16.26c0.78,2.93,2.49,5.41,5.42,6.19 C12.21,47.87,34,48,34,48s21.79-0.13,27.1-1.55c2.93-0.78,4.64-3.26,5.42-6.19C67.94,34.95,68,24,68,24S67.94,13.05,66.52,7.74z"
fill="#fe0001"
></path>
<path d="M 45,24 27,14 27,34" fill="#fff"></path>
</svg>
</div>
並在 element 綁定點擊事件,進行 iframe 的置換。
elm.addEventListener("click", function (event) {
var wrapper = event.currentTarget;
var src = wrapper.getAttribute("data-src");
if (src) {
wrapper.removeAttribute("data-src");
var iframe = document.createElement("iframe");
iframe.src = src + "?autopaly=1";
wrapper.innerHTML = "";
wrapper.appendChild(iframe);
}
});
如果你不想自行實作,也有 lite-youtube-embed Web Component 可以使用,各大前端框架也有出相關的 library。
但動態載入 iframe 還有其他的問題,在 iOS 下會無法使用自動播放功能需要點擊兩次的問題,因此可能要改使用 YouTube iframe api js library 來載入 YouTube iframe,來解決這個問題,但即便使用 library,我自己實測仍有釋出零星 case 無法自動播放。
考量到 mobile 上螢幕較小,若你的網頁並沒有需求希望使用者不要跳離,那我會建議在 mobile 上直接用 link 取代 iframe,這樣有以下的好處:
- 另開網頁就可以自動播放,影片大小比較不會像是 iframe 受到排版限制。
- 可藉由 Universal Link 跟 native YouTube App 整合,App 的 操作介面比 Web 更加的順暢。
- 如果使用者有無廣告的 Premium 會員,跳到 App 就可以直接享有本身無廣告的功能。
- 從 App 切換回原本網頁也可以有 Picture in Picture 的下方縮小播放模式。
因此用 anchor 取代原本的 div 加上標題與連結如下:
<div class="ytWrapper">
<a
href="https://www.youtube.com/watch?v=cowtgmZuai0"
target="_blank"
class="ytContent"
rel="noreferer"
data-src="https://www.youtube.com/embed/cowtgmZuai0"
>
<img
src="https://i.ytimg.com/vi/cowtgmZuai0/hqdefault.jpg"
loading="lazy"
decoding="async"
/>
<svg viewBox="0 0 68 48" version="1.1" height="48" width="68">
<path
d="M66.52,7.74c-0.78-2.93-2.49-5.41-5.42-6.19C55.79,.13,34,0,34,0S12.21,.13,6.9,1.55 C3.97,2.33,2.27,4.81,1.48,7.74C0.06,13.05,0,24,0,24s0.06,10.95,1.48,16.26c0.78,2.93,2.49,5.41,5.42,6.19 C12.21,47.87,34,48,34,48s21.79-0.13,27.1-1.55c2.93-0.78,4.64-3.26,5.42-6.19C67.94,34.95,68,24,68,24S67.94,13.05,66.52,7.74z"
fill="#fe0001"
></path>
<path d="M 45,24 27,14 27,34" fill="#fff"></path>
</svg>
</a>
<a
href="https://www.youtube.com/watch?v=cowtgmZuai0"
target="_blank"
class="ytTitle"
rel="noreferer"
>
Tabs vs. Spaces
</a>
<a
href="https://www.youtube.com/watch?v=cowtgmZuai0"
target="_blank"
class="ytLink"
rel="noreferer"
>
<span>Watch on</span>
<svg viewBox="0 0 110 26" version="1.1" height="16px" width="72px">
<path
fill="#fff"
d="M 16.68,.99 C 13.55,1.03 7.02,1.16 4.99,1.68 c -1.49,.4 -2.59,1.6 -2.99,3 -0.69,2.7 -0.68,8.31 -0.68,8.31 0,0 -0.01,5.61 .68,8.31 .39,1.5 1.59,2.6 2.99,3 2.69,.7 13.40,.68 13.40,.68 0,0 10.70,.01 13.40,-0.68 1.5,-0.4 2.59,-1.6 2.99,-3 .69,-2.7 .68,-8.31 .68,-8.31 0,0 .11,-5.61 -0.68,-8.31 -0.4,-1.5 -1.59,-2.6 -2.99,-3 C 29.11,.98 18.40,.99 18.40,.99 c 0,0 -0.67,-0.01 -1.71,0 z m 72.21,.90 0,21.28 2.78,0 .31,-1.37 .09,0 c .3,.5 .71,.88 1.21,1.18 .5,.3 1.08,.40 1.68,.40 1.1,0 1.99,-0.49 2.49,-1.59 .5,-1.1 .81,-2.70 .81,-4.90 l 0,-2.40 c 0,-1.6 -0.11,-2.90 -0.31,-3.90 -0.2,-0.89 -0.5,-1.59 -1,-2.09 -0.5,-0.4 -1.10,-0.59 -1.90,-0.59 -0.59,0 -1.18,.19 -1.68,.49 -0.49,.3 -1.01,.80 -1.21,1.40 l 0,-7.90 -3.28,0 z m -49.99,.78 3.90,13.90 .18,6.71 3.31,0 0,-6.71 3.87,-13.90 -3.37,0 -1.40,6.31 c -0.4,1.89 -0.71,3.19 -0.81,3.99 l -0.09,0 c -0.2,-1.1 -0.51,-2.4 -0.81,-3.99 l -1.37,-6.31 -3.40,0 z m 29.59,0 0,2.71 3.40,0 0,17.90 3.28,0 0,-17.90 3.40,0 c 0,0 .00,-2.71 -0.09,-2.71 l -9.99,0 z m -53.49,5.12 8.90,5.18 -8.90,5.09 0,-10.28 z m 89.40,.09 c -1.7,0 -2.89,.59 -3.59,1.59 -0.69,.99 -0.99,2.60 -0.99,4.90 l 0,2.59 c 0,2.2 .30,3.90 .99,4.90 .7,1.1 1.8,1.59 3.5,1.59 1.4,0 2.38,-0.3 3.18,-1 .7,-0.7 1.09,-1.69 1.09,-3.09 l 0,-0.5 -2.90,-0.21 c 0,1 -0.08,1.6 -0.28,2 -0.1,.4 -0.5,.62 -1,.62 -0.3,0 -0.61,-0.11 -0.81,-0.31 -0.2,-0.3 -0.30,-0.59 -0.40,-1.09 -0.1,-0.5 -0.09,-1.21 -0.09,-2.21 l 0,-0.78 5.71,-0.09 0,-2.62 c 0,-1.6 -0.10,-2.78 -0.40,-3.68 -0.2,-0.89 -0.71,-1.59 -1.31,-1.99 -0.7,-0.4 -1.48,-0.59 -2.68,-0.59 z m -50.49,.09 c -1.09,0 -2.01,.18 -2.71,.68 -0.7,.4 -1.2,1.12 -1.49,2.12 -0.3,1 -0.5,2.27 -0.5,3.87 l 0,2.21 c 0,1.5 .10,2.78 .40,3.78 .2,.9 .70,1.62 1.40,2.12 .69,.5 1.71,.68 2.81,.78 1.19,0 2.08,-0.28 2.78,-0.68 .69,-0.4 1.09,-1.09 1.49,-2.09 .39,-1 .49,-2.30 .49,-3.90 l 0,-2.21 c 0,-1.6 -0.2,-2.87 -0.49,-3.87 -0.3,-0.89 -0.8,-1.62 -1.49,-2.12 -0.7,-0.5 -1.58,-0.68 -2.68,-0.68 z m 12.18,.09 0,11.90 c -0.1,.3 -0.29,.48 -0.59,.68 -0.2,.2 -0.51,.31 -0.81,.31 -0.3,0 -0.58,-0.10 -0.68,-0.40 -0.1,-0.3 -0.18,-0.70 -0.18,-1.40 l 0,-10.99 -3.40,0 0,11.21 c 0,1.4 .18,2.39 .68,3.09 .49,.7 1.21,1 2.21,1 1.4,0 2.48,-0.69 3.18,-2.09 l .09,0 .31,1.78 2.59,0 0,-14.99 c 0,0 -3.40,.00 -3.40,-0.09 z m 17.31,0 0,11.90 c -0.1,.3 -0.29,.48 -0.59,.68 -0.2,.2 -0.51,.31 -0.81,.31 -0.3,0 -0.58,-0.10 -0.68,-0.40 -0.1,-0.3 -0.21,-0.70 -0.21,-1.40 l 0,-10.99 -3.40,0 0,11.21 c 0,1.4 .21,2.39 .71,3.09 .5,.7 1.18,1 2.18,1 1.39,0 2.51,-0.69 3.21,-2.09 l .09,0 .28,1.78 2.62,0 0,-14.99 c 0,0 -3.40,.00 -3.40,-0.09 z m 20.90,2.09 c .4,0 .58,.11 .78,.31 .2,.3 .30,.59 .40,1.09 .1,.5 .09,1.21 .09,2.21 l 0,1.09 -2.5,0 0,-1.09 c 0,-1 -0.00,-1.71 .09,-2.21 0,-0.4 .11,-0.8 .31,-1 .2,-0.3 .51,-0.40 .81,-0.40 z m -50.49,.12 c .5,0 .8,.18 1,.68 .19,.5 .28,1.30 .28,2.40 l 0,4.68 c 0,1.1 -0.08,1.90 -0.28,2.40 -0.2,.5 -0.5,.68 -1,.68 -0.5,0 -0.79,-0.18 -0.99,-0.68 -0.2,-0.5 -0.31,-1.30 -0.31,-2.40 l 0,-4.68 c 0,-1.1 .11,-1.90 .31,-2.40 .2,-0.5 .49,-0.68 .99,-0.68 z m 39.68,.09 c .3,0 .61,.10 .81,.40 .2,.3 .27,.67 .37,1.37 .1,.6 .12,1.51 .12,2.71 l .09,1.90 c 0,1.1 .00,1.99 -0.09,2.59 -0.1,.6 -0.19,1.08 -0.49,1.28 -0.2,.3 -0.50,.40 -0.90,.40 -0.3,0 -0.51,-0.08 -0.81,-0.18 -0.2,-0.1 -0.39,-0.29 -0.59,-0.59 l 0,-8.5 c .1,-0.4 .29,-0.7 .59,-1 .3,-0.3 .60,-0.40 .90,-0.40 z"
></path>
</svg>
</a>
</div>
並檢查螢幕寬度來判斷是否使用要使用 iframe 置換,或採取原生 link 跳轉:
ytContent.addEventListener("click", function (event) {
const isMobile = window.matchMedia("(max-width: 767px)").matches;
if (isMobile) {
return;
}
event.preventDefault();
var ytWrapper = ytContent.parentElement;
var src = ytContent.getAttribute("data-src");
if (src) {
ytContent.removeAttribute("data-src");
var iframe = document.createElement("iframe");
iframe.src = src + "?autopaly=1";
ytWrapper.textContent = "";
ytWrapper.appendChild(iframe);
}
});
Demo:
embeded Tweet iframe 解決方案 #
Twitter 本身其實有提供 API 給開發者取的推文資料,第一步你必須先去 https://developer.twitter.com/en/portal/dashboard 開 project 申請開發者 token。
npm i -s twitter-api-v2
就可以透過下面的方式取得推文資料,你就可以自己製作推文 html preview link,取代原本的 iframe
const { TwitterApi } = require("twitter-api-v2");
const twitterClient = new TwitterApi(TWITTER_DEV_TOKEN);
const roClient = twitterClient.readOnly;
const result = await roClient.v2.singleTweet(id, {
"media.fields": ["preview_image_url", "type", "url", "width", "height"],
"tweet.fields": ["attachments", "public_metrics", "created_at"],
"user.fields": ["id", "url", "profile_image_url", "description"],
expansions: [
"referenced_tweets.id",
"attachments.media_keys",
"in_reply_to_user_id",
"author_id",
],
});
Demo:
效能比較 #
我把本頁面取代 iframe 的 與原 iframe 頁面中的 script runtime 與 js size 做了個比較如下表:
js size | script runtime | |
---|---|---|
facade | 2 KB | 4 ms |
youtube-embed | 762 KB | 530 ms |
tweet-embed | 401 KB | 614 ms |
codesandbox-embed | 1.9 MB | 1620 ms |
對於不想要看影片或是使用 iframe 的使用者來說,效能跟體積上 facade 靜態 element 都 iframe 快了百倍有餘。
結語 #
直接嵌入 iframe 對網頁初始化效能有害,透過提供預覽內容的 facade element 能夠有效減少網頁初始化時 iframe 外部加載成本,而且由於是同頁面的 element,即可提供瀏覽器更穩定的排版計算避免 RWD 時 iframe 長寬比難以計算的問題。如果你被 iOS 動態載入 YouTube iframe 無法自動播放困擾,也可以參考我文中提供的解決方法。