实际遇到的问题
浏览器同域并发请求数量限制
不同厂商的浏览器,对同一域名下不同请求的并发数量都有限制(见下图),以chrome为例,chrome同域名的并发请求限制数量为6个,也就是说,当前同一个个域名下,一旦存在等待响应的请求数量达到6个,那么后面该域名的其他请求,必须进入等待队列,待前面的请求完成后,才会被执行。
Browser | HTTP 1.1 |
---|---|
IE6,7 | 4 |
IE8 | 6 |
IE9 | 10 |
IE10,IE11 | 6 |
Firefox | 6 |
Safari | 4 |
Chrome | 6 |
Opera | 4 |
同域并发请求数量限制,不只针对接口请求,页面上的静态资源请求,包括图片,音视频,css/js文件等,都会适用同样的策略限制。
项目中存在多个音视频同时展示的场景
在一些业务场景中,系统需要兼容旧版本的浏览器内核,且根据业务的不同场景,有可能会出现一个页面同时需要展示多个音视频的场景,在此业务场景下,有一定概率会触发浏览器同域并发请求数量限制策略。
当触发浏览器同域并发请求数量限制时,前面6个音视频未完成请求前,后面的音视频请求均在等待队列,都无法加载元数据(音频无法获取metaData得到时长信息,视频无法获取metaData得到时长和视频首图信息),要前面的请求完成以后,才能获取到上述信息,在此场景下,用户无法通过音视频在页面上展示的元数据来判断音视频文件是否正常,超过限制数量的音视频无法被加载元数据。
低版本浏览器对音视频加载策略存在缺陷
经过实验,发现(Chrome18-64)版本内核对音视频加载存在以下缺陷:
- HTMLMediaElement preload 默认设置为auto
H5中,
枚举值 | 说明 |
---|---|
none | 示意用户可能不会播放该音视频,或者服务器希望节省带宽;换句话说,该音视频不会被缓存 |
metadata | 示意即使用户可能不会播放该音视频,但获取元数据 (例如音视频长度) 还是有必要的 |
auto | 示意用户可能会播放音视频;换句话说,如果有必要,整个音视频都将被加载,即使用户不期望使用 |
因此,Chrome18-64等低版本浏览器,遇到每个音视频会默认尝试加载整个音视频,当触发浏览器同域并发请求数量限制策略时,且加载整个音视频数据比较耗时,就会明显影响到后续音视频的加载。 2. preload枚举值配置为metadata无效
经过实验测试,Chrome18-64将
chrome39版本下,当音视频资源请求超过2.2M,请求会在获取到2.2M数据后,主动挂起,且不会释放请求,因此改请求会一直处于未完成状态,并一直占用该域名的请求数,当类似请求达到浏览器同域并发请求限制数量时,后面的请求则永远在排队,导致类卡死现象,除非手动触发下前面音视频的播放,让挂起的请求继续完成请求,才能释放该请求,后续请求才能执行。举例:当用户上传了6个超过2.2M的音频后,上传第7个音频时,就会一直卡住,无法上传,出现这种情况,后面相同域名的请求就会一直卡在等待中,无法发送请求。
小结
为了解决低版本浏览器的同域并发请求限制策略和多媒体请求挂起策略导致的资源加载和请求卡死问题,本文介绍一种通用、低耦合、高兼容、可配置、即插即用的浏览器同域并发请求优化方案。
优化方案介绍
概要
本文介绍的是一种通用、低耦合、高兼容、可配置、即插即用的处理低版本浏览器同域并发请求优化方案,接入项目只需要在页面上引入media.js文件(大小572 B),即可实现低版本浏览器同域并发请求的优化。
行业一般解决方案
行业中,一般采用多域名/cdn模式来解决浏览器同域并发请求限制。因为浏览器并发请求限制只针对同一个域名,因此,不同域名的并发请求数量不会受限,举个例子,在浏览器同域并发请求显示数为6的前提下,页面需要同时展示7个图片,那么7个图片资源url可以分配两个不同的域名,这样,同一个域名的并发请求不超过6个,则这7个图片资源都可以同时加载出来。
举例:京东有多个静态资源域名,如京东首页上的商品图片,某个商品图片的地址为:https://img11.360buyimg.com/jdcms/s230x230_jfs/t1/243937/35/5048/89258/65e9640eF6e7ac006/e24f5453c106b713.jpg 我们把域名img11改成img12、img13、img14都能请求到相同的图片,因此当img11并发请求达到6个的时候,其他图片资源可以通过其他域名(img12、img13、img14)来获取,域名不同,则不会受到浏览器同域并发请求限制。
实际方案评估
作者评估了域名申请,CDN,修改本地host文件来控制域名等方案,这些方案或无法同时满足这三种作者所在项目的部署场景,或会额外带来成本,或会额外带来失效风险,因此,评估后,决定使用纯前端的方案来处理。
前端浏览器同域并发请求优化方案整体思路
监听dom变化,如果媒体对象被创建,则给媒体对象添加suspend监听**事件。当浏览器加载媒体资源时过程中触发主动挂起(上面提到的超过2.2M音频文件加载到2.2M会被浏览器挂起,无法释放请求),会触发suspend事件,因此,我们在suspend事件中,我们通过代码逻辑触发,让资源继续加载(防止出现挂起后占用浏览器同域并发请求数量)。
具体实现
chrome版本配置
const VERSION = 39; // 配置需要优化的最高版本 const userAgent = navigator.userAgent; const chromeVersionRegex = /Chrome/([0-9]+)./; const match = userAgent.match(chromeVersionRegex);
if (match) {
const chromeVersion = match[1];
if (Number(chromeVersion) <= VERSION) {
// 执行浏览器同域并发请求优化逻辑
// …
}
}
dom树变化监听
使用Web API MutationObserver监听dom树更新,当dom 有子节点(childList)变化时,执行callback回调,在回调中对音视频加载进行处理。
const targetNode = document.querySelector('body');
const config = {
childList: true
};
const callback = (mutationsList, observer) => {
// dom更新回调
// ...
};
const observer = new MutationObserver(callback);
observer.observe(targetNode, config);
媒体挂起监听
遍历音视频dom节点,给所有音视频节点添加suspend的监听事件,当音视频被挂起时(Chrome39下超过2.2M的音视频资源会在2.2M处主动触发挂起),此时调用function bufferMedia 主动触发音视频继续缓冲,可以释放出当前音视频占用的同域并发请求数量。
const medias = document.querySelectorAll('audio, video');
if (medias.length > 0) {
const _loop = (i) => {
if (!medias[i].onsuspend && (medias[i].preload !== 'matedata' && medias[i].preload !== 'none')) {
medias[i].onsuspend = function () {
bufferMedia(medias[i]); // 监听suspend事件逻辑
};
}
};
for (let i = 0; i < medias.length; i++) {
_loop(i);
}
}
触发继续下载
通过对比音视频总时长duration和音视频缓冲区结束点时间,判断音视频是否已经完成下载,低版本浏览器(Chrome39)在音视频缓冲完成(视频时长=缓冲区结束点时间)后,才能释放占用的同域并发请求限制数量。
function bufferMedia(media) {
if (media.duration > media.buffered.end(0)) {
if (media.paused) {
media.currentTime = media.buffered.end(0) + 0.01; // 通过修改当前时间到缓冲结束时间后触发继续下载
}
}
if (media.duration === media.buffered.end(0)) {
// 音频/视频已缓冲完毕, 更新时间控制标记
media.paused() && media.attributes['data-timechange'].value = '1'
}
}
多媒体组件封装
封装原h5
Media.js
"use strict";var targetNode=document.querySelector("body"),config={childList:!0},callback=function(e,o){console.log("dom 修改了");var n=document.querySelectorAll("audio");if(n&&n.length>0)for(var r=function(e){n[e].onsuspend||(n[e].onsuspend=function(){console.log("音频/视频频挂起"),n[e].duration>n[e].buffered.end(0)&&n[e].paused&&(n[e].currentTime=n[e].buffered.end(0)+.01),n[e].duration==n[e].buffered.end(0)&&console.log("音频/视频已缓冲完毕")})},t=0;t
这是一个从 https://juejin.cn/post/7368355894709551144 下的原始话题分离的讨论话题