直接嵌入 iframe 對網頁效能有害 - Jason's Web Memo

直接嵌入 iframe 對網頁效能有害

一般來說大家習慣在網頁上嵌入 YouTube 或是 third-party resource 像是 Twitter 的貼文,以至於我們寫範例常用的 CodeSandbox,大部分都是以 iframe 嵌入網頁的方式來使用,原因不外乎簡單好用又方便。但你是否曾經想過這些 iframe 事實上相當吃資源,且讓你的使用者數據變成數位廣告投遞的來源的風險。

embeded iframe 有多大

我把下列 embeded iframe 頁面單獨拉出來用 chrome 測出來的 network 流量與效能數據。

所得到的數據如下:

js sizescript runtime
youtube-embed762 KB530 ms
tweet-embed401 KB614 ms
codesandbox-embed1.9 MB1602 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,這樣有以下的好處:

  1. 另開網頁就可以自動播放,影片大小比較不會像是 iframe 受到排版限制。
  2. 可藉由 Universal Link 跟 native YouTube App 整合,App 的 操作介面比 Web 更加的順暢。
  3. 如果使用者有無廣告的 Premium 會員,跳到 App 就可以直接享有本身無廣告的功能。
  4. 從 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:

Ben Lesh
@Ben Lesh
Periodic reminder that tabs are definitively better than spaces for one reason alone: Accessibility. Users can change the display size of tabs to whatever suits their needs. This can be helpful for people with reading or vision difficulties.
02:24 PM, Feb 02, 2022
10 Retweets 3 Quote Tweets 83 Likes 10 Replys

效能比較

我把本頁面取代 iframe 的 與原 iframe 頁面中的 script runtime 與 js size 做了個比較如下表:

js sizescript runtime
facade2 KB4 ms
youtube-embed762 KB530 ms
tweet-embed401 KB614 ms
codesandbox-embed1.9 MB1620 ms

對於不想要看影片或是使用 iframe 的使用者來說,效能跟體積上 facade 靜態 element 都 iframe 快了百倍有餘。

結語

直接嵌入 iframe 對網頁初始化效能有害,透過提供預覽內容的 facade element 能夠有效減少網頁初始化時 iframe 外部加載成本,而且由於是同頁面的 element,即可提供瀏覽器更穩定的排版計算避免 RWD 時 iframe 長寬比難以計算的問題。如果你被 iOS 動態載入 YouTube iframe 無法自動播放困擾,也可以參考我文中提供的解決方法。

Ref

Webmention 社群迴響 22

喜歡 18
轉推 1
引用或評論 3

用 Webmentions 參與社群迴響

透過 twitter 轉推,你的轉推評論之後會更新在上面社群迴響的引用評論列表。

轉推這篇文章

如果你的 blog 文章想要引用本文,歡迎透過下方表單用將你的 blog 文章網址傳送給我,若你的 blog 文章含有正確的本文網址連結,並且 blog 文章本身支持 microformat,之後你的引用資訊會更新在上面社群迴響的引用評論列表。

社群迴響將不定期更新,不保證同步,同時有資料缺漏的可能性。最新回應請參考以下連結:

Jason Chen - Yahoo Taiwan EC Web frontend engineer currently. Write something about web and React.js here

Jason Chen

Yahoo Taiwan Sr. Frontend Engineer. Write something about web and React.js here.

訂閱 blog 更新 開啟小鈴鐺

複製 RSS xml 網址

訂閱 Google Groups 電子報

追蹤我的 Medium

--

Other Posts