一个大视频的背景,若是作的好,会是一个绝佳的体验!可是,在首页添加一个视频并不只仅是随便找我的,而后加个 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:欢迎你们关注个人公众号【前端下午茶】,一块儿加油吧~
另外能够加入「前端下午茶交流群」微信群,长按识别下面二维码便可加我好友,备注加群,我拉你入群~