Promise.all 的异步图像预加载不适用于非 Chrome 浏览器
Async image preloading by Promise.all not working on non-Chrome browsers
const preloadImg = (...urls) => {
const toolDiv = document.createElement('div');
toolDiv.style = 'display: none';
const load = url => {
return new Promise(res => {
const img = new Image();
img.src = url;
img.onload = () => res(img);
});
};
const getImgs = imgs => {
const promises = imgs.map(async url => {
const img = await load(url);
toolDiv.appendChild(img);
});
return Promise.all(promises);
}
getImgs(urls).then(() => {
document.body.appendChild(toolDiv);
});
};
您确实在 Elements
中看到了包含所有这些图像的 <div>
,并且路径是正确的:
<div style="display: none"> // I try removing <display: none> but not working, either
<img src="img/329b774421235a3b27d7142b1707ea01.jpg">
<img src="img/33df62feaa1871d7ff4c2b933aa82992.jpg">
<img src="img/5681a01b46e89618d96ff523dc81a1fb.jpg">
<img src="img/183ad681c899a84c82e288ac8ad30604.jpg">
<img src="img/1d9c8fdb875c31cbfa1f83e11a7038af.jpg">
<img src="img/71b1abb6a445059bf43463ab80e75506.jpg">
<img src="img/40734ae2e8255713e76814eba786f018.jpg">
<img src="img/8c40e3bdea0a863b76d888ad9952cf74.jpg">
<img src="img/8aa56b4e08ef9a40c92e6e0609991280.jpg">
</div>
同样在 Network
中,您可以看到所有请求都已正确获取(来自 Edge 18 的屏幕截图,其中预加载不起作用):
异步预加载仅在 Chrome(和 Opera)中有效,任何其他浏览器(ff、edge、ie)都会导致闪烁,就像根本没有预加载一样(但显示时没有额外的 http 请求,这意味着那些图像被提取和缓存)。
我检查了 Promise
、Promise.all
和 async await
的浏览器支持,没有问题。
然而,传统的同步预加载在浏览器中完美运行:
urls.forEach(url => {
const img = new Image();
img.src = url;
toolDiv.appendChild(img);
});
document.body.appendChild(toolDiv);
这些图像是由带有 Babel(core-js 和运行时)的 Webpack 文件加载器处理的,但我认为这不是问题所在。
需要帮助谢谢!!
更新:我只是借了室友的iPhone8,没有闪烁。
我会在这里说得更清楚。这些图像显示为背景图像,由 css classes 切换并由鼠标滚轮触发,例如:
/*.css */
.img1::before,
.img1::after {
background-image: url(../img/img1.jpg); /* Would be ../img/dasdfsafadasdasda.jpg after file-loader */
}
.img2::before,
.img2::after {
background-image: url(../img/img2.jpg);
}
<!-- .html -->
<aside class="img1" id="aside"></aside>
// .js
something.onwheel = () => {
document.getElementById('aside').className = 'img2';
};
通过 class 重命名切换图像。
但我仍然认为这个 class 开关也不是问题。
最后更新:@Kaiido 的方法有效。此问题是由获取的图像的部分可用性引起的。完全可用性需要以下解码过程。 whatwg
你在这里面对的是 load event 只告诉我们资源已经被获取并且浏览器能够处理媒体。
还有一大步:图像解码.
事实上,即使所有资源都已从服务器获取并且浏览器可以从文件的 headers 中解析,它也能够解码它,以及其他数据,如媒体的维度,一些浏览器会等到真正需要它时再尝试实际解码图像数据(如果你愿意的话,"pixels")。
这个过程仍然需要时间,并且在您将所有这些 元素实际附加到文档之前,这些浏览器将开始执行该操作,因此会出现闪烁。
现在,为什么 Chrome 没有遇到这个问题?因为正如 this very related Q/A 中所展示的那样,它们在触发 load
事件之前确实对图像进行了解码。这种策略有利有弊,规范目前只要求 no-decode 行为。
现在,有解决该问题的方法,如先前链接的 Q/A:
所示
在支持浏览器的情况下,可以进一步等待HTMLImageElement.decode()
promise,这会强制浏览器对图片进行完全解码,从而在promise resolve后直接绘制:
img.onload = (evt) => {
img.decode().then(() => res(img));
};
// using big images so the latency is more visible
imgs = `https://upload.wikimedia.org/wikipedia/commons/d/dc/Spotted_hyena_%28Crocuta_crocuta%29.jpg
https://upload.wikimedia.org/wikipedia/commons/3/37/Mud_Cow_Racing_-_Pacu_Jawi_-_West_Sumatra%2C_Indonesia.jpg
https://upload.wikimedia.org/wikipedia/commons/c/cf/Black_hole_-_Messier_87.jpg`.split(/\s+/g);
const preloadImg = (...urls) => {
const toolDiv = document.createElement('div');
toolDiv.style = 'display: none';
const load = url => {
return new Promise(res => {
const img = new Image();
// we disable cache for demo
img.src = url + '?r=' + Math.random();
// further wait for the decoding
img.onload = (evt) => {
console.log('loaded data of a single image');
img.decode().then(() => res(img));
};
});
};
const getImgs = imgs => {
const promises = imgs.map(async url => {
const img = await load(url);
toolDiv.appendChild(img);
});
return Promise.all(promises);
}
getImgs(urls).then(() => {
document.body.appendChild(toolDiv);
toolDiv.style.display = "";
console.log("all done");
});
};
d.onclick = () => Array.from(x = document.querySelectorAll('div')).forEach(x => x.parentNode.removeChild(x));
c.onclick = () => {
preloadImg(...imgs);
};
preloadImg(...imgs);
img {
width: 100vw
}
<button id="c">click</button><button id="d">click</button>
如果您需要支持旧版浏览器,您可以使用 HTMLCanvasElement 很容易地对其进行猴子修补:
if( !HTMLImageElement.prototype.decode ) {
const canvas = document.createElement( 'canvas' );
canvas.width = canvas.height = 1; // low memory footprint
const ctx = canvas.getContext('2d');
HTMLImageElement.prototype.decode = function() {
return new Promise( ( resolve, reject ) => {
setTimeout( () => { // truly async
try {
ctx.drawImage(this,0,0);
resolve()
}
catch( err ) {
reject( err );
}
}, 0 );
} );
};
}
凯多的回答如果有效请采纳。
采纳了凯多的建议。 Chrome 上仍然闪烁。把它放在这里,这样它就不会弄乱 OP 的主要 post.
imgs=`https://i.imgur.com/OTQMjbE.jpg
https://i.imgur.com/gUpn5Jf.jpg
https://i.imgur.com/sIXWJWD.jpg
https://i.imgur.com/qhzfDD6.jpg`.split(/\s+/g);
const preloadImg = (...urls) => {
const toolDiv = document.createElement('div');
toolDiv.style = 'display: none';
const load = url => {
return new Promise(res => {
const img = new Image();
img.src = url;
// using decode
img.onload = (evt) => {
img.decode().then(() => res(img));
};
});
};
const getImgs = imgs => {
const promises = imgs.map(async url => {
const img = await load(url);
toolDiv.appendChild(img);
});
return Promise.all(promises);
}
getImgs(urls).then(() => {
document.body.appendChild(toolDiv);
});
};
preloadImg(...imgs);
let i = 1;
document.body.onwheel = () => {
document.getElementById('aside').className = 'img' + (i++%4+1)
};
/*.css */
.img1::before,
.img1::after,.img1 {
background-image: url(https://i.imgur.com/sIXWJWD.jpg); /* Would be ../img/dasdfsafadasdasda.jpg after file-loader */
}
.img2::before,
.img2::after,.img2 {
background-image: url(https://i.imgur.com/qhzfDD6.jpg);
}
.img3::before,
.img3::after,.img3 {
background-image: url(https://i.imgur.com/OTQMjbE.jpg);
}
.img4::before,
.img4::after,.img4 {
background-image: url(https://i.imgur.com/gUpn5Jf.jpg);
}
body {
height:5000px
}
#aside {
width: 500px;
height: 500px;
}
<div><aside class="img1" id="aside"></aside></div>
const preloadImg = (...urls) => {
const toolDiv = document.createElement('div');
toolDiv.style = 'display: none';
const load = url => {
return new Promise(res => {
const img = new Image();
img.src = url;
img.onload = () => res(img);
});
};
const getImgs = imgs => {
const promises = imgs.map(async url => {
const img = await load(url);
toolDiv.appendChild(img);
});
return Promise.all(promises);
}
getImgs(urls).then(() => {
document.body.appendChild(toolDiv);
});
};
您确实在 Elements
中看到了包含所有这些图像的 <div>
,并且路径是正确的:
<div style="display: none"> // I try removing <display: none> but not working, either
<img src="img/329b774421235a3b27d7142b1707ea01.jpg">
<img src="img/33df62feaa1871d7ff4c2b933aa82992.jpg">
<img src="img/5681a01b46e89618d96ff523dc81a1fb.jpg">
<img src="img/183ad681c899a84c82e288ac8ad30604.jpg">
<img src="img/1d9c8fdb875c31cbfa1f83e11a7038af.jpg">
<img src="img/71b1abb6a445059bf43463ab80e75506.jpg">
<img src="img/40734ae2e8255713e76814eba786f018.jpg">
<img src="img/8c40e3bdea0a863b76d888ad9952cf74.jpg">
<img src="img/8aa56b4e08ef9a40c92e6e0609991280.jpg">
</div>
同样在 Network
中,您可以看到所有请求都已正确获取(来自 Edge 18 的屏幕截图,其中预加载不起作用):
异步预加载仅在 Chrome(和 Opera)中有效,任何其他浏览器(ff、edge、ie)都会导致闪烁,就像根本没有预加载一样(但显示时没有额外的 http 请求,这意味着那些图像被提取和缓存)。
我检查了 Promise
、Promise.all
和 async await
的浏览器支持,没有问题。
然而,传统的同步预加载在浏览器中完美运行:
urls.forEach(url => {
const img = new Image();
img.src = url;
toolDiv.appendChild(img);
});
document.body.appendChild(toolDiv);
这些图像是由带有 Babel(core-js 和运行时)的 Webpack 文件加载器处理的,但我认为这不是问题所在。
需要帮助谢谢!!
更新:我只是借了室友的iPhone8,没有闪烁。
我会在这里说得更清楚。这些图像显示为背景图像,由 css classes 切换并由鼠标滚轮触发,例如:
/*.css */
.img1::before,
.img1::after {
background-image: url(../img/img1.jpg); /* Would be ../img/dasdfsafadasdasda.jpg after file-loader */
}
.img2::before,
.img2::after {
background-image: url(../img/img2.jpg);
}
<!-- .html -->
<aside class="img1" id="aside"></aside>
// .js
something.onwheel = () => {
document.getElementById('aside').className = 'img2';
};
但我仍然认为这个 class 开关也不是问题。
最后更新:@Kaiido 的方法有效。此问题是由获取的图像的部分可用性引起的。完全可用性需要以下解码过程。 whatwg
你在这里面对的是 load event 只告诉我们资源已经被获取并且浏览器能够处理媒体。
还有一大步:图像解码.
事实上,即使所有资源都已从服务器获取并且浏览器可以从文件的 headers 中解析,它也能够解码它,以及其他数据,如媒体的维度,一些浏览器会等到真正需要它时再尝试实际解码图像数据(如果你愿意的话,"pixels")。
这个过程仍然需要时间,并且在您将所有这些 元素实际附加到文档之前,这些浏览器将开始执行该操作,因此会出现闪烁。
现在,为什么 Chrome 没有遇到这个问题?因为正如 this very related Q/A 中所展示的那样,它们在触发 load
事件之前确实对图像进行了解码。这种策略有利有弊,规范目前只要求 no-decode 行为。
现在,有解决该问题的方法,如先前链接的 Q/A:
所示在支持浏览器的情况下,可以进一步等待HTMLImageElement.decode()
promise,这会强制浏览器对图片进行完全解码,从而在promise resolve后直接绘制:
img.onload = (evt) => {
img.decode().then(() => res(img));
};
// using big images so the latency is more visible
imgs = `https://upload.wikimedia.org/wikipedia/commons/d/dc/Spotted_hyena_%28Crocuta_crocuta%29.jpg
https://upload.wikimedia.org/wikipedia/commons/3/37/Mud_Cow_Racing_-_Pacu_Jawi_-_West_Sumatra%2C_Indonesia.jpg
https://upload.wikimedia.org/wikipedia/commons/c/cf/Black_hole_-_Messier_87.jpg`.split(/\s+/g);
const preloadImg = (...urls) => {
const toolDiv = document.createElement('div');
toolDiv.style = 'display: none';
const load = url => {
return new Promise(res => {
const img = new Image();
// we disable cache for demo
img.src = url + '?r=' + Math.random();
// further wait for the decoding
img.onload = (evt) => {
console.log('loaded data of a single image');
img.decode().then(() => res(img));
};
});
};
const getImgs = imgs => {
const promises = imgs.map(async url => {
const img = await load(url);
toolDiv.appendChild(img);
});
return Promise.all(promises);
}
getImgs(urls).then(() => {
document.body.appendChild(toolDiv);
toolDiv.style.display = "";
console.log("all done");
});
};
d.onclick = () => Array.from(x = document.querySelectorAll('div')).forEach(x => x.parentNode.removeChild(x));
c.onclick = () => {
preloadImg(...imgs);
};
preloadImg(...imgs);
img {
width: 100vw
}
<button id="c">click</button><button id="d">click</button>
如果您需要支持旧版浏览器,您可以使用 HTMLCanvasElement 很容易地对其进行猴子修补:
if( !HTMLImageElement.prototype.decode ) {
const canvas = document.createElement( 'canvas' );
canvas.width = canvas.height = 1; // low memory footprint
const ctx = canvas.getContext('2d');
HTMLImageElement.prototype.decode = function() {
return new Promise( ( resolve, reject ) => {
setTimeout( () => { // truly async
try {
ctx.drawImage(this,0,0);
resolve()
}
catch( err ) {
reject( err );
}
}, 0 );
} );
};
}
凯多的回答如果有效请采纳。
采纳了凯多的建议。 Chrome 上仍然闪烁。把它放在这里,这样它就不会弄乱 OP 的主要 post.
imgs=`https://i.imgur.com/OTQMjbE.jpg
https://i.imgur.com/gUpn5Jf.jpg
https://i.imgur.com/sIXWJWD.jpg
https://i.imgur.com/qhzfDD6.jpg`.split(/\s+/g);
const preloadImg = (...urls) => {
const toolDiv = document.createElement('div');
toolDiv.style = 'display: none';
const load = url => {
return new Promise(res => {
const img = new Image();
img.src = url;
// using decode
img.onload = (evt) => {
img.decode().then(() => res(img));
};
});
};
const getImgs = imgs => {
const promises = imgs.map(async url => {
const img = await load(url);
toolDiv.appendChild(img);
});
return Promise.all(promises);
}
getImgs(urls).then(() => {
document.body.appendChild(toolDiv);
});
};
preloadImg(...imgs);
let i = 1;
document.body.onwheel = () => {
document.getElementById('aside').className = 'img' + (i++%4+1)
};
/*.css */
.img1::before,
.img1::after,.img1 {
background-image: url(https://i.imgur.com/sIXWJWD.jpg); /* Would be ../img/dasdfsafadasdasda.jpg after file-loader */
}
.img2::before,
.img2::after,.img2 {
background-image: url(https://i.imgur.com/qhzfDD6.jpg);
}
.img3::before,
.img3::after,.img3 {
background-image: url(https://i.imgur.com/OTQMjbE.jpg);
}
.img4::before,
.img4::after,.img4 {
background-image: url(https://i.imgur.com/gUpn5Jf.jpg);
}
body {
height:5000px
}
#aside {
width: 500px;
height: 500px;
}
<div><aside class="img1" id="aside"></aside></div>