背景
今天遇到了一个问题,打开一个页面后网站挂了.Network里查看,后台有一张图片请求一直在pending,打开此页面后的后续其他的请求均没有响应,但经过验证这并非后台的服务器挂了,没返回图片响应.因为直接关闭当前的tab页,新开一个tab页,其他页面还是可以正常加载的,服务器也都正常响应。只是这个浏览器标签好像卡死了.
此页面与其他页面的区别在于并发请求比较多。所以我怀疑是ajax请求数量超过了chrome的最大并发数6,导致chrome的标签页卡死。因此尝试限制ajax请求的并发数量,此文记录过程.
模拟并发请求
为此,我模拟了并发请求,并且对并发请求进行限制.
这是没有经过限制的并发请求瀑布图.可以看出原始的并发请求在chrome浏览器下最大并发数为6,其余请求将会放在队列中,上面的6个请求中结束了一个会从队列中取出一个进行请求.
var promiseArr = [];
for(let i = 0;i<12;i++){
promiseArr.push(function(){
return new Promise(function(resolve,reject){
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
if(xhr.readyState===4 && xhr.status===200){
resolve(xhr.responseText)
}
}
xhr.open('GET', 'https://hacker-news.firebaseio.com/v0/topstories.json', true)
xhr.send();
})
}())
}
Promise.all(promiseArr);

图1.原始并发请求
我还在火狐浏览器下测试了一下,也是类似的结果。

根据[1]提供的方法,把火狐浏览器的并发数修改成8以后,再次测试。

对并发请求进行限制
我参考了这里的代码:不到50行代码实现一个能对请求并发数做限制的通用RequestDecorator
class RequestDecorator {
constructor ({
maxLimit = 5,
requestApi,
needChange2Promise,
}) {
this.maxLimit = maxLimit;
// 一个虚假的队列,里面压入的是现生成的promise,用于在达到最大并发数时卡住不向下进行.之前的请求结果返回一个,才能count--,然后调用next函数,resolve队列里的一个promise,继续执行代码,发起新的请求.
this.requestQueue = [];
this.currentConcurrent = 0;
this.requestApi = needChange2Promise ? pify(requestApi) : requestApi;
}
async request(...args) {
if (this.currentConcurrent >= this.maxLimit) {
await this.startBlocking();
}
try {
this.currentConcurrent++;
const result = await this.requestApi(...args);
return Promise.resolve(result);
} catch (err) {
return Promise.reject(err);
} finally {
console.log('当前并发数:', this.currentConcurrent);
this.currentConcurrent--;
this.next();
}
}
startBlocking() {
let _resolve;
let promise2 = new Promise((resolve, reject) => _resolve = resolve);
this.requestQueue.push(_resolve);
return promise2;
}
next() {
if (this.requestQueue.length <= 0) return;
const _resolve = this.requestQueue.shift();
_resolve();
}
}
module.exports = RequestDecorator;
完整代码如下:
<!DOCTYPE html>
<html>
<head>
<title></title>
<!--为了可以在ie上运行-->
<script src="http://cdn.jsdelivr.net/bluebird/3.5.0/bluebird.min.js"></script>
</head>
<body>
<script type="text/javascript">
class RequestDecotration {
constructor(max) {
this.max = max;
this.waitQueue = [];
this.currentIndex = 0;
}
async request(caller, ...args) {
if (this.currentIndex >= this.max) {
await this.wait();
}
this.currentIndex++;
try {
const res = await caller();
//返回一个thenable的promise对象。为了和axios返回的结果类型保持一致
return Promise.resolve(res)
} catch (error) {
return Promise.reject(error)
} finally {
//经历过了return的竟然还能到finally。。。涨姿势了。
this.currentIndex--;
this.next();
}
}
wait() {
let tempResolve = null;
let promise = new Promise((resolve, reject) => { tempResolve = resolve })
this.waitQueue.push(tempResolve);
return promise
}
next() {
if (this.waitQueue.length > 0) {
let _resolve = this.waitQueue.shift();
_resolve();
}
}
}
var promiseArr = [];
for(let i = 0;i<12;i++){
promiseArr.push(function(){
return new Promise(function(resolve,reject){
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
if(xhr.readyState===4 && xhr.status===200){
resolve(xhr.responseText)
}
}
xhr.open('GET', 'https://hacker-news.firebaseio.com/v0/topstories.json', true)
xhr.send();
})
})
}
//这里传入限并发数的参数
let requestDecotration = new RequestDecotration(3);
promiseArr.forEach(xhrPromise=>requestDecotration.request(xhrPromise));
</script>
</body>
</html>

图2.并发数为5

图3.并发数为4

图4.并发数为3

图5.并发数为2
真实页面的瀑布图

上图是真实页面的请求瀑布图,限制最大并发数为2.可以看出这个代码其实并非完全每一次都并发2个,而是上2个请求中的一个请求结束了,会开始下一个请求.可以从画的竖线上看出来

上图依然设置了最大并发数为2,为什么看起来没有第一张图的并发看起来明显呢?如刚才说,因为上2个请求中的第二个请求一直没返回响应,所以要等第三个请求结束才能开始第四个请求,第四个请求结束开始第五个请求,结果就变成了一个一个请求的发起,直到第二个请求返回响应结果.

不过我们也能看到限制了并发数后,最后一个图片semi-transparent请求依然是pending的状态,页面还是卡死的状态.所以此文使用限制并发数的方法对解决此场景下的问题没有帮助,不过也是值得记录学习的.
最后此问题的出现原因经过排查在于,后端返回图片是计算的content-length大小与实际图片大小不等,仔细看会发现实际请求中包含了3个图片请求.虽然图片在chrome上都显示出来了.但实际上请求并未结束,chrome下会提示:
Caution: request is not finished yet
这在Chrome/edge/firefox等浏览器上图片都会正确显示,从network里看请求列表也不容易发现问题.只有点进去请求详情,点到time里,才能看出端倪.但是诚实的ie直接拒绝显示这3张图片,这才发现了问题.此处,感谢单纯没心机的ie.