一個大視頻的背景,若是作的好,會是一個絕佳的體驗!可是,在首頁添加一個視頻並不只僅是隨便找我的,而後加個 25mb 的視頻,那會讓你的全部的性能優化都付之一炬。javascript
Lazy pandas love lazy loading. (Photo by Elena Loshina)css
我參加過一些團隊,他們但願給首頁加上相似的全屏視頻背景。我一般不肯意那麼作,由於這種作法一般會致使性能上的噩夢。老實說,我曾給一個頁面加上一個 40mb 大的視頻。 😬html
上次有人讓我這麼作的時候,我很好奇應如何將背景視頻的加載做爲漸進加強(Progressive Enhancement),來提高網絡鏈接情況比較好的用戶的體驗。除了和個人同事們強調視頻體積小和壓縮視頻的重要性之外,也但願在代碼上有一些奇蹟發生。前端
下面是最終的解決方案:java
<source>
canplaythrough
事件canplaythrough
事件沒有在 2 秒內觸發,那麼使用 Promise.race()
將視頻加載超時canplaythrough
事件,那麼移除 <source>
,而且取消視頻加載canplaythrough
事件,那麼使用淡入效果顯示這個視頻這裏要注意的問題是,即便我正在 <video>
標籤中使用 <source>
,但我還沒爲這些 <source>
設置 src
屬性。若是設置了 src
屬性,那麼瀏覽器會自動地找到它能夠播放的第一個 <source>
,並當即開始下載它。android
由於在這個例子中,視頻是做爲漸進加強的對象,默認狀況下咱們不用真的加載視頻。事實上惟一須要加載的,是咱們爲這個頁面設置的預覽圖片。ios
<video class="js-video-loader" poster="<?= $poster; ?>" muted="true" loop="true"> <source data-src="path/to/video.webm" type="video/webm"> <source data-src="path/to/video.mp4" type="video/mp4"> </video>
我編寫了一個簡單的 JavaScript 類,用於查找帶有 .js-video-loader
這個 class 的 video 元素,讓咱們之後能夠在其餘視頻中複用這個邏輯。完整的源碼能夠從 Github 上看到。git
構造函數是這樣的:github
constructor () { this.videos = Array.from(document.querySelectorAll('video.js-video-loader')); // 將在下面狀況下返回 // - 瀏覽器不支持 Promise // - 沒有 video 元素 // - 若是用戶設置了減小動態偏好(prefers reduced motion) // - 在移動設備上 if (typeof Promise === 'undefined' || !this.videos || window.matchMedia('(prefers-reduced-motion)').matches || window.innerWidth < 992 ) { return; } this.videos.forEach(this.loadVideo.bind(this)); }
這裏咱們所作的就是找到這個頁面上全部咱們但願延遲加載的視頻。若是沒有,咱們能夠返回。當用戶開啓了減小動態偏好(preference for reduced motion)設置時,咱們一樣不會加載這樣的視頻。爲了避免讓某些低網速或低圖形處理能力的手機用戶擔憂,在小屏幕手機上也會直接返回。(我在考慮是否能夠經過 <source>
元素的媒體查詢來作這些,但也不肯定。)web
而後給每一個視頻運行這個視頻加載邏輯。
loadVideo()
是一個調用其餘函數的簡單的函數:
loadVideo(video) { this.setSource(video); // 加上了視頻連接後從新加載視頻 video.load(); this.checkLoadTime(video); }
在 setSource()
中,咱們找到那些做爲數據屬性(Data Attributes)插入的視頻連接,而且將它們設置爲真正的 src
屬性。
/** * 找 video 子元素中是 <source> 的, * 基於 data-src 屬性, * 給每一個 <source> 設置 src 屬性 * * @param {DOM Object} video */ setSource (video) { let children = Array.from(video.children); children.forEach(child => { if (child.tagName === 'SOURCE' && typeof child.dataset.src !== 'undefined') { child.setAttribute('src', child.dataset.src); } }); }
基本上,我所作的就是遍歷每個 <video>
元素的子元素,找一個定義了 data-src
屬性(child.dataset.src
)的 <source>
子元素。若是找到了,那就用 setAttribute
將它的 src
屬性設置爲視頻連接。
如今視頻連接已經被設置給 <video>
元素了,下面須要讓瀏覽器再次加載視頻。咱們經過在 loadVideo()
中的 video.load()
來完成這個工做。load()
方法是 HTMLMediaElement API 的一部分,它能夠重置媒體元素而且重啓加載過程。
接下來是見證奇蹟的時刻。在 checkLoadTime()
方法中咱們建立了兩個 Promise。第一個 Promise 將在 <video>
元素的 canplaythrough 事件觸發時被 resolve
。這個 canplaythrough
事件是瀏覽器認爲這個視頻能夠在不停下來緩衝的狀況下持續播放的時候被觸發。咱們在這個 Promise 中添加一個這個事件的監聽回調,當這個事件觸發的時候執行 resolve()
。
// 建立一個 Promise,將在 // video.canplaythrough 事件發生時被 resolve let videoLoad = new Promise((resolve) => { video.addEventListener('canplaythrough', () => { resolve('can play'); }); });
咱們同時建立另外一個 Promise 做爲計時器。在這個 Promise 中,當通過一個設定好的時間後,咱們使用 setTimeout
來將這個 Promise 給 resolve 掉,我這設置了一個 2 秒的時延(2000毫秒)。
// 建立一個 Promise 將在 // 特定時間(2s)後被 resolve let videoTimeout = new Promise((resolve) => { setTimeout(() => { resolve('The video timed out.'); }, 2000); });
如今咱們有了兩個 Promise,咱們能夠經過 Promise.race()
看他們誰先完成。
// 將 promises 進行 Race 看看哪一個先被 resolves Promise.race([videoLoad, videoTimeout]). then(data => { if (data === 'can play') { video.play(); setTimeout(() => { video.classList.add('video-loaded'); }, 3000); } else { this.cancelLoad(video); } });
在這個 .then()
的回調中咱們等着拿到最早被 resolve
的那個 Promise 傳回來的信息。若是這個視頻能夠播放,那麼我就會拿到以前傳的 can play
,而後試一下是否能夠播放這個視頻。video.play()
是使用 HTMLMediaElement 提供的 play()
方法來觸發視頻播放。
3 秒後,setTimeout()
將會給這個標籤加上 .video-loaded
類,這將有助於視頻文件更巧妙的淡入自動循環播放。
若是咱們沒接收到 can play
字符串,那麼咱們將取消這個視頻的加載。
cancelLoad()
方法作的基本上跟 loadVideo()
方法相反。它從每一個 source
標籤移除 src
屬性,而且觸發 video.load()
來重置視頻元素。
若是咱們不這麼作,這個視頻元素將會在後臺保持加載狀態,即便咱們都沒將它顯示出來。
/** * 經過移除全部的 <source> 來取消視頻加載 * 而後觸發 video.load(). * * @param {DOM object} video */ cancelLoad (video) { let children = Array.from(video.children); children.forEach(child => { if (child.tagName === 'SOURCE' && typeof child.dataset.src !== 'undefined') { child.parentNode.removeChild(child); } }); // 從新加載沒有 <source> 標籤的 video // 這樣它會中止下載 video.load(); }
這個方法的缺點是,咱們仍然試圖經過一個不必定靠譜的連接來下載一個可能比較大的文件,可是經過提供一個超時時間,咱們但願可以給某些網速慢的用戶節約一些流量而且得到更好的性能。根據我在 Chrome Dev Tools 裏將網速節流到慢 3G 條件下的測試,這個方法將在超時以前加載了 512kb 的視頻。即便是一個 3-5mb 的視頻,對於一些網速慢的用戶來講,這也帶來了顯著的流量節省。
你以爲怎麼樣?若是有改進的建議,歡迎在評論裏分享!
Originally published at benrobertson.io.
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、 iOS、 前端、 後端、 區塊鏈、 產品、 設計、 人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、 官方微博、 知乎專欄。
PS:歡迎你們關注個人公衆號【前端下午茶】,一塊兒加油吧~
另外能夠加入「前端下午茶交流羣」微信羣,長按識別下面二維碼便可加我好友,備註加羣,我拉你入羣~