React downloads pending images of previous render - javascript

(Update: previously, I thought this problem was caused by React Router, but I have stripped React Router out of the code, and the problem still persists. So I have modified this question thoroughly.)
Situation:
I have paginated pages which show a list of images per page. (With "page" I mean the complete content that is visible, I don't mean a separate html file/url.) I would like to nagivate through those pages in an efficient manner.
Problem:
If I navigate quickly enough through the pages, not all images will be loaded in the current page before navigating to the next page. I would expect the browser to cancel all pending unfinished image downloads when navigating to the next page. But this doesn't happen, the browser keeps all the unfinished images pending to be downloaded, until they are all downloaded. Then the images of the page to which I navigated, will be downloaded. This causes a big delay and wasted bandwidth.
Question:
Is it possible to cancel the downloading of "pending" images of the previous page?
Demonstration code:
To test this, use the "network" tab in the developer tool of you browser. Also choose "disable cache" and set the throttle (download speed in developer tool) to something slow like "Slow 3G" otherwise the images will be loaded to fast too see the problem. Then navigate through the pages and see that the list of pending images is stacking up, then click the "disable images" button. Then, no image will be visible on the screen, but the browser still has a large number of pending image downloads open, which is wasted bandwith and causes a delay when new images need to be rendered.
You can test the code here: https://codepen.io/Devabc/pen/PowjqwZ
//This code is using imgur images to demonstrate.
class Page extends React.Component {
state = {
pageNr: 1,
imagesEnabled: true
};
onLinkClick = event => {
const number = event.target.dataset.value;
console.log("Number: " + number);
this.setState({pageNr: number});
};
onButtonClick = event => {
console.log("toggling images");
this.setState(prevState => {
return {imagesEnabled: ! prevState.imagesEnabled};
});
};
render() {
const links = _.range(0, 5).map(number => {
return (
<a href="#" data-value={number} onClick={this.onLinkClick}>
{number}
</a>
);
});
const pageNr = this.state.pageNr;
const imgStart = pageNr * 100;
const imgEnd = imgStart + 100;
const images = this.state.imagesEnabled
? <ImagesPanel imgStart={imgStart} imgEnd={imgEnd} />
: null;
return (
<div>
<h1>Page {this.state.pageNr}</h1>
<div>Page links: {links}</div>
<div><button onClick={this.onButtonClick}>{this.state.imagesEnabled ? "disable images" : "enable images"}</button></div>
<div>Images:</div>
<div>{images}</div>
</div>
);
}
}
function ImagesPanel(props) {
const images = _.range(props.imgStart, props.imgEnd).map(number => {
return (
<Image imageNr={number} />
);
});
return images;
}
function imageUrl(imageNr) {
const hash = imgurHashes[imageNr];
return "https://i.imgur.com/" + hash + ".jpg";
}
function Image(props) {
const url = imageUrl(props.imageNr);
return <img class="myImage" src={url} border='1' />
}
const imgurHashes = ["wmk2tcs","jvqH2X4","r3dz09r","yJYRtvI","33bUPXj","cYsggBH","URAl4lS","xBpS7lq","5LMFxjU","kUrFsMB","GZf8FnO","Er2lmge","22CbMOq","vJcKGb3","U9ALJof","LxfGswQ","YzyyFHI","vin2W11","c0PQRSY","b6b2qva","6UmvLGc","oTtDO7S","LGoOzDl","XD9o83i","dMUi3dj","XpvMqXC","9JYf6o2","8IBe95g","X26sUn3","qb7Taz9","lWd5TCZ","2UKHpPZ","PMmglpV","pZ5ukGv","ymEZize","nYURuNZ","1SToTrZ","GZHTkpe","NH0qm8I","mZRTNyB","FBAoint","nJWbHb9","BI9zvXf","OeT5kWf","JZ1WPQA","6ZK3S2x","z6M8ryn","3yMODr2","bUoicZu","p3ReIJA","dybF5Sh","DH5ZBEH","fMEbpy5","UyMkbSp","EKXikAy","YG3aJm9","4JWIQhV","lgsvM63","A0MepAi","957yfQF","iNkwwNi","aaJpoxO","Vxy5RgX","jZxV1kQ","JuTUNdL","WY4e0cg","xmgTP7A","O35FJpg","VA3fFhv","oGZVPYQ","X9PRsWA","wSYxWzX","xntVddT","rDn0s52","vQPT1rH","GmlqCZt","zntCiSZ","SoEHjHB","bTFF7HW","QjJRzmx","DZxjoKZ","XdIYgsc","aBZChfb","rAIuEHZ","zt9EJD9","vaEJQA1","9c5pjUL","VXW3Ubz","315aFBb","klp7nh9","fsZktFx","x1XmXYX","8HaInVG","6jJOtkE","9aElwYX","R4dDTw7","9hgY0kI","SW8M5sw","R9jcSrP","dlSSb0P","bhxZCLX","mogQ5tz","oHxiJye","PYyOOm3","Ns89wvi","2uWIFDx","nXN5uhB","UMQn1yZ","JCAEJ3a","VTlkiOu","5JyWm3p","RVE8GoO","XVje7aY","C6qr30z","e54jc67","0X4cRbP","qZaU4lT","WUPHAQD","iILnFAb","oCsxMga","hQyN9oX","GQYDzhE","RcTO075","JlTn7jd","0jJHUWZ","iKp7JGx","YaQeN45","Ot5nFpM","8CeXfPJ","m3cQQye","JYXQaj7","pzPglwg","B4a34uo","3dHvLPv","FEn3aTc","coi9Jpy","GU5ih0m","CtnHd4Y","hbOFsRS","xNW9ED4","avCP3XP","mcbapy0","r9E6DqQ","JDjounb","xHGiHZR","LPN7uSK","QwwPGKE","OGTUcVh","OLxQHfA","Mg0QT9y","0ia7Ca3","LKKHYJn","W8nyx6Q","FjLCYY4","5YEYOe9","vLQHljC","jyL5CS9","oCuKxVQ","L2IyiSA","ffkgeN2","FF7bKmo","JpWF0LA","72kPNNw","tmdNh8K","7PBAKy4","EXMlyuO","p5ZkX5b","Iilf92H","fQbVFiU","wj1csk6","rP14xLY","1iQM4nW","XMq6P6Y","dTRijsS","B3Sz2J8","UkSipp2","eAiZQlr","JJkbcAs","sfA0TJ4","foex9pW","IoCvWfI","5yAxTX7","H6EAfeQ","k8d637d","yIQVrZH","bQdJFx5","CiPGw2A","YZLiutP","BQlEKfd","0W032dR","r4PWPJF","mGBYym5","BC7cuX5","TcoRdCZ","KsCfq9T","KaoAUfC","RdDFY7O","HFhBXSb","kOkYQua","mmT60dj","vIqZffx","r7T9b7N","y1Qtgh9","GFwT6Zc","nWUqQQ3","OZMUUjP","x4ocIKv","wy13lyN","fTj8Om8","0AgyD0C","prWbjvJ","MNJdsJk","brS1te1","4xP3P3i","IlcxIrq","bX7vGyi","U4mhrTE","FVMGYVw","zRFW4oG","jpbvgCe","ZJJEUHu","aU5C2XY","jfFsb9b","WfkChky","qNKDvPo","fhgzlkL","4uEHjGz","cRPiOZz","OzA6TSu","iEhVkeo","tXCZZQY","7DBLToo","9rrTZl5","FWO4ugI","kel6MJM","md4dsMN","kvfKaxR","JMyKehy","3jlMpQk","r1DSqFd","ywuONdJ","eCfhJ5y","vDmQ7Sm","BS6VgDy","wwa7mX9","4E1aIuK","D0zqMuZ","ovehv4U","s3yrw4S","GXm3DCm","gugRkdk","6H2WA7e","M6qdVzv","59aJXiY","As2zA50","wyc2LnT","IuhXtFA","l3V5mZS","QoUlc4T","L7XNlhe","cNk7D2j","MPF5iQY","wRjmFCp","7PZSZrU","EpSMAzy","hLaPhDJ","bS3dM0R","SOCUNXb","Q7BIVld","3Yrg85R","cX4KejE","IQbsyz1","i1qMSgy","K5cU3Qb","NeeB0tr","YHZIvUY","I4BmxjZ","BtThGp0","qWPlax5","pDFqfpR","SdET6fw","9EpcwDb","6nDPyRo","B4X4pYN","mkYU9mK","P6A7I0V","Gwb8Wtr","kSkjNsR","vlEb6D8","Gznsyvk","Vpm3QdO","949USnU","8HyKw0G","4tJmhAP","GLXxX89","X4GGU1t","wRVqclU","rEFHH4Q","vLmSGxD","gkFI4kz","kQASxFm","Cq5brtp","KmWYkSo","2IjiGnN","laGj46X","mgYgH2n","I2iwVFj","oFHxy9R","8VWomE0","y1kV08m","PqXAGBv","MNoiTk8","qEEpz2x","e4ipNRw","CfVVnG8","hHnuzRP","YthPF8u","YBigt6A","tQI8AFu","2K0TnUO","GYReGhp","F2XQStI","7j7145H","BVkgYv1","4hfVk8z","faw1ajs","27H4ogL","0a9ZQ2T","041BIAs","dilQYiq","P1e5AXA","5Tia8aA","PFNJRet","t1Nhl5f","UpfH1sp","H2zXF5f","HLdaLwV","30O8VSP","KsaM1XE","jAhqN6P","Yoi2ylE","wkrg47U","ePmLF64","KLSJPrd","aUdvQ0a","VcqZhpH","zEqDYjX","WXLTAbp","shny07g","UqlXlR5","Bpw7KnV","YeP3Zhe","C5NSYDy","Y6GzbOA","6FxTImw","6PXXJmd","FnVsEu0","Ll2zJnC","JvNbUcB","nEOXggf","t28tvPa","m2qG0pl","KjkkYG5","kCcUVrD","g2G823J","ZyD1f4Z","P3SfyR5","QsA5vHF","DpSxArz","dKt3T4S","w0GfNCC","kvyacI9","Gqdr7qd","KlHHMzZ","VVZ7HVw","hFG5mwe","D4S9tPV","SMVSpk3","MYzi6pi","BzQeber","v7E7VCw","a03HdC6","KSAiRwR","7WkzcMx","z8iVQZP","qCDNLpI","TUerfpq","X08dKd5","qkmDrys","k0MjIxD","vIgbAlY","15TeGJg","j7vx4tU","DNvqPee","2jEQHaF","5Q1M9HL","EFninXx","0VttnZV","XKgdZGL","fDSOKh8","1i5Fhk1","wmUU0tL","O7SVAVF","WMoTmty","UUVTKaF","n9EPI8R","QqnEITA","lao3U57","ITabVKK","ph97wz5","bFfdCGt","9KWJKs3","aKKQd6o","jxzaSfj","6Gv8gBL","iDS0A1T","bTAS7ej","5FvLcFu","GtIFmNQ","kJAU7gn","UZCPmpz","68yuNFU","TuO5PNi","lMV0piH","taKCu3Y","PmNa3M4","Z6gErEZ","5qrVmOD","N5yxt2l","LhrvwLr","QhBn6p8","2kpPEe8","dnxlCK3","GMDnQ32","3qH62l5","Jy6aHR2","2tIZHcB","w0zzrUJ","aSh1mwr","fwCCSBI","k5osQCu","byHHnMX","Uu9Dq9I","K9sC2OO","CsFf1Kz","G28GqCu","OOPc79G","be5NrVR","C5XAmr2","rDNSwSj","AI4BrBq","hGwueuh","EpG6zfG","QORwSKm","sWDMpiD","U2QfeTp","kUqUudt","PGMbcrN","bEaFtGN","KPrPXxO","4iFaBhm","fDcFhWG","P1M2Ld5","aPAlbHH","8ye6kdq","ztVBpFQ","SVL8ujT","5CwT2Og","nIqakeV","SM3Jcoz","QdAk2M4","zpLnMrH","fglMOex","ynj6fe7","YG1DOZh","aJ50pkC","SbvVCaf","azLfxiY","gdw8DHE","1U00sfi","p3zgLIS","h9cTNnw","Z1tt3RC","HHnBLCI","hmkeUl1","aMKRR0h","6MteQjh","PMZzXiM","v2uh5Mk","QEDz82m","70LhmSw","KEGMEbg","tlG769G","gyoNASp","AUnDdta","z1TZP9m","nVmmkCH","IIDRcHT","8m1Go3S","LsscGjy"];
ReactDOM.render(
<Page />,
document.getElementById('root')
);
The screenshot below shows the list of pending images in the browser, even though no images should be rendered at this moment, because the React component that should render the images is already unmounted/removed.

The problem is not caused by React. I'm not sure who to blame for this problem, it could be the Document Object Model standard, HTML or the browsers implementing it. The problem is that once an image is attached to the DOM (when the browser starts to render a HTML page), the browsers will start to download it, even when the image is later removed from the DOM.
There are however a number of solutions to solve this problem.
Solution 1: src attribute modification to cancel downloads
The src attribute is mandatory in the img element of HTML, but setting it to an empty string (clearing), results in cancellation of the download of the image. (This may not work on all browsers.)
This solution is described here: How to cancel an image from loading and demonstrated here https://jsbin.com/zakeqaseru/1/edit?output and here https://jsfiddle.net/nw34gLgt/
(Use the browser developer tools to monitor whether the image download is cancelled.)
The src can be changed to the empty string using setAttribute(). Setting it to null has the same effect as setting it to the string "null", which would also work, but is less clean.
Removing the src attribute doesn't work on most browsers.
Whether this solution works may differ per browser. Older browsers may cause the web page itself to be requested when src is removed, as mentioned here: https://gtmetrix.com/avoid-empty-src-or-href.html
This could be a very big performance hit on both the brower and the web server.
In case the browser cannot deal with this, a similar solution would be to use the background-image property on a HTML element like a div instead of an img.
This src-clearing solution is implemented in React-Image:
https://github.com/mbrevda/react-image/pull/223
this.i.src = ''
try {
delete this.i.src
} catch (e) {
// On Safari in Strict mode this will throw an exception,
// - https://github.com/mbrevda/react-image/issues/187
// We don't need to do anything about it.
}
delete this.i
A solution in React would be something like this:
class Img extends React.Component {
constructor(props) {
super(props);
this.imgRef = React.createRef();
}
componentWillUnmount() {
try {
this.imgRef.current.src = '';
delete this.imgRef.current.src;
} catch (e) {}
}
render() {
return (
<img {...this.props} ref={this.imgRef} />
);
}
}
Then you can use it like this:
<Img src="...some image url...." />
Solution 2: lazy loading src
By setting the src attribute under a programmatic condition, you can make sure that images are only loaded when needed. This solution can also be combined with other solutions to make sure that images that are downloaded, are cancelled when the associated React elements are unmounted.
The data-src (custom data) attribute is often used in this approach, to copy its value to src when an image needs to be rendered.
This solution may still download unmounted images.
Solution 3: Chrome's native lazy-loading
This solution exists since Chrome 76 (July 2019?). Chrome has a loading attribute for images and iframes, which can be set to "lazy" to defer downloading of the resource until it reaches a calculated distance from the viewport.
The loading attribute isn't supported by many other browser.
For more info, see:
https://web.dev/native-lazy-loading/
https://github.com/scott-little/lazyload/
Lazy-loading request for Firefox: https://bugzilla.mozilla.org/show_bug.cgi?id=1542784
This solution may still download unmounted images.
Additional information sources
https://developers.google.com/web/fundamentals/performance/lazy-loading-guidance/images-and-video
https://imagekit.io/blog/lazy-loading-images-complete-guide/

Related

Is a js event emitted when a css font is swapped?

Can I tell when a font has been successfully (or otherwise) loaded, and then act on that with JS?
Context
I'm printing a series of documents using playwright. I'm currently loading these fonts https://fonts.googleapis.com/css2?family=Raleway:wght#100;300;600&display=swap and sometimes the waitUntil="networkidle" will complete its ½ second wait before the font has loaded, resulting in the doc being printed in the fallback font.
References
I've had a read of this: Controlling Font Performance with font-display and this: Navigating & waiting, and done a lot of searching, but with no avail.
Possible work arounds
I could download the font to my computer so that it's a local asset, but that means remembering an extra step if I change my mind about the font in future. It also makes it harder to do on a remote machine.
I could add in a fat pad of >4 seconds as an explicit wait, but that adds 4×N seconds to the run time if N=the number of documents (6½ per 100 documents)
I could write a service worker to precache the fonts and then emit an event, but this is way more work than this deserves, and managing its lifecycle seems like a painful future.
Is there an easy way?
Yes you can do this with Font Face Observer which is a small #font-face to monitor the load of the font. This does not restrict you using any type of font loading.
for example
var font = new FontFaceObserver('My Family', {
weight: 400
});
font.load().then(function () {
console.log('Font is available');
}, function () {
console.log('Font is not available');
});
If you want more information check out https://portalzine.de/dev/options-to-detect-when-a-font-face-has-been-loaded/
Hope that answered your question.
The link in christopher-holder's answer pointed me at that useful article from Portalzine. I used the technique from their first option, i.e.
alert('Roboto loaded? ' + document.fonts.check('1em Roboto')); // false
document.fonts.ready.then(function () {
alert('All fonts in use by visible text have loaded.');
alert('Roboto loaded? ' + document.fonts.check('1em Roboto')); // true
});
document.fonts.onloadingdone = function (fontFaceSetEvent) {
alert('onloadingdone we have ' + fontFaceSetEvent.fontfaces.length + ' font faces loaded');
};
and moved the logic to the Playwright script using page.waitForFunction like this:
await page.goto(
"file:///" + path.resolve(htmlFilename),
(waitUntil = "networkidle")
);
await page.waitForFunction(() => document.fonts.check("1em Raleway"));
This waits for the page to finish loading, and for the network to be quiet for ½ a second, and then checks if the font is loaded.
FontFaceObserver looks nice, but this approach keeps the printing logic in the playwright script and doesn't touch the document itself, which feels cleaner.
This might be belt and braces, I'll update this answer once I've tested it more thoroughly.
Here's a piece of JS code that I'm using to re-adjust the scroll position to adjust for :target scroll-margin and web fonts that differ from fallback fonts:
if (location.hash)
{
var targetElement = document.getElementById(location.hash.substring(1));
if (targetElement && targetElement.scrollIntoView)
{
// scroll to correct position immediately
targetElement.scrollIntoView({block:"start", behavior:"auto"});
// Note that because the page might be rendered before
// web fonts are ready and web fonts may/will cause
// layout shift, we'll need to re-adjust the scroll
// position when fonts are ready:
try
{
var fontsReady = document.fonts.ready;
fontsReady.then(function ()
{
console.log('Font loading complete');
// process full event loop and re-adjust the scroll:
window.setTimeout(function ()
{
targetElement.scrollIntoView({block:"nearest", behavior:"smooth"});
}, 0);
});
}
catch (e)
{
console && console.error && console.error(e);
console && console.log && console.log("This browser doesn't support observing font loading, scroll position may be incorrect.");
}
}
}
Depending on your use case and expected event timing both calls to scrollIntoView() could use behavior:"auto" or behavior:"smooth". Also note that the user may have scrolled the viewport between rendering the content with fallback fonts and web font loading completing so you may want to add additional code to listen for scroll events and avoid scrolling anything when web fonts are swapped in if user has already scrolled to another position.

Image loading issue with vue

This issue might not be specific to vue but here goes...
I'm searching for some items and retrieving them from a database, each item has a unique name which I use to load their image from an external site, for example:
<img :src="'https://external-site.com/photos/' + item.name + '.jpg'" />
Whenever I search for the first item it returns the item with it's image and details. But whenever I search for a second item, it returns the right details but uses the cached image of the last item until it's own image has loaded.
I decided I would use some events on the image to show a loader before they start loading but I only found that there were three events specific to images: onabort, onerror, onload.
But I need an event to show a loader at the start of downloading the image. If not, is there another way I can resolve this issue?
A common trick to defeating the cache is to add an innocuous, changing parameter to your url, such as a timestamp:
<img :src="'https://external-site.com/photos/' + item.name + '.jpg?x=' + Date.now()" />
The parameter shouldn't interfere with accessing the image, but the browser won't assume the url is the same.
I solved the issue by using Progressive image rendering with vue:
First I installed a package that gave me a v-lazy-image component by npm i v-lazy-image
then I imported the component
<script>
import VLazyImage from "v-lazy-image";
export default {
components: {
VLazyImage
}
};
</script>
The component then allows you to specify the image and a placeholder image to use while the image loads:
<v-lazy-image
:src="'https://external-site.com/photos/' + item.name + '.jpg'"
src-placeholder="/images/default.jpg"
/>
You can see more details of this component here
I had exactly the same issue. And none of the previous answers solved my problem.
At the method that change your items details and images link (ex.: changeDetails()), make a backup of your real image path (impath) at (impath_b) and set your image path to null. Vue.js will draw a transparent image, and after 100ms the real image path (impath) is restored.
changeDetails() {
// for ...
item.impath_b = item.impath; // it's not necessary declare impath_b before.
item.impath = null;
// ...
// just before the method ends:
setTimeout(function () {
vm.revertImages();
}, 100);
},
revertImages() {
// for ...
item.impath = item.impath_b;
item.impath_b = null;
}
At the html code show the image just when it is not null:
<img v-if="item.file!=null" :src="item.file"/>
<img v-else :src="transpGif"/>
Define transpGif at your variables area:
transpGif: 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==',
more details about this transpGif here .
The idea is to clean and redraw the images with an empty image, and then the new image will be drawn when it be loaded.

check if multiple images loaded within certain timeinterval; if not then replace with other url

Problem- I am displaying some images on a page which are being served by some proxy server. In each page I am displaying 30 images ( 6 rows - 5 in each row). Here if due to overload or due to any other issue if proxy server could not able to server images( either all images or some of them) in 6 seconds then I want to replace unloaded image url with some other url using javascript so that I could display 30 images at the end.
What I tried is below.
objImg = new Image();
objImg.src = 'http://www.menucool.com/slider/prod/image-slider-4.jpg';
if(!objImg.complete)
{
alert('image not loaded');
}else{
img.src = 'http://www.menucool.com/slider/prod/image-slider-4.jpg';
}
I also tried with below code.
$('img[id^="picThumbImg_"]').each(function(){
if($(this).load()) {
//it will display loaded image id's to console
window.console.log($(this).attr('id'));
}
});
I could not use set time-out for each image because it will delay all page load.
I checked other similar question on stack-overflow but no solution worked me perfectly as I need to display multiple images.Please guide how to proceed.
You don't have to wait 6 seconds, or using TimeOut. You can check if the images are loaded or not using the onload Javascript/Jquery event. I know, it will take a little bit to dispatch the onerror event, let see:
Why don't use the load Jquery event on the window or the image itself?
$(window).load(function(){
//check each image
})
Disadvantage:
It will wait for other resources like scripts, stylesheets & flash, and not just images, which may or may not be OK to you.
If the image loads from the cache, some browsers may not fire off the event (included yours and that's why your code is not working)
Why don't use the error Jquery event on the image itself?
$('img[id^="picThumbImg_"]').error(function(){
//image loading error
})
Disadvantages:
It doesn't work consistently nor reliably cross-browser
It doesn't fire correctly in WebKit if the image src is set to the same src as before
It doesn't correctly bubble up the DOM tree
Can cease to fire for images that already live in the browser's cache
Note:: Error is almost the same that the load event
Improving the code!:
$('img[id^="picThumbImg_"]').one('error', function() {
// image load error
}).each(function() {
if(!this.complete) $(this).error();
});
This will avoid few things of the previous code, but you still will have to wait if it's a 404 and you're replacing it in the onerror event, that will take a little bit right?
So, what now!
You can use this awesome plugin!. Once you add the reference, you just have to use something like:
var imgLoad = imagesLoaded('#img-container');
imgLoad.on( 'always', function() {
// detect which image is broken
for ( var i = 0, len = imgLoad.images.length; i < len; i++ ) {
if(!imgLoad.images[i].isLoaded){
//changing the src
imgLoad.images[i].img.src = imgLoad.images[i].img.getAttribute("data-src2");
}
}
});
Your HTML markup should look like:
<div id="img-container">
<div class="row">
...
</div>
<div class="row">
<img src="original-path.jpg" data-src2="alternative-path.jpg">
...
</div>
<div class="row">
...
</div>
</div>
Note: You don't need jQuery in this case and this plugin is suggested by Paul Irish ;)
Give all your images a specific class. Loop through your images and use .load() to check if loaded, example below...
Detect image load

How do I abort image <img> load requests without using window.stop()

I have a very long page that dynamically loads images as users scroll through.
However, if a user quickly scrolls away from a certain part of the page, I don't want the images to continue loading in that now out-of-view part of the page.
There are lots of other requests happening on the page simultaneously apart from image loading, so a blunt window.stop() firing on the scroll event is not acceptable.
I have tried removing & clearing the img src attributes for images that are no longer in view, however, since the request was already started, the image continues to load.
Remember that the image src was filled in as the user briefly scrolled past that part of the page. Once past though, I couldn't get that image from stop loading without using window.stop(). Clearing src didn't work. (Chrome & FF)
Similar posts I found that get close, but don't seem to solve this problem:
Stop loading of images with javascript (lazyload)?
Javascript: Cancel/Stop Image Requests
How to cancel an image from loading
What you are trying to do is the wrong approach, as mentioned by nrabinowitz. You can't just "cancel" the loading process of an image (setting the src attribute to an empty string is not a good idea). In fact, even if you could, doing so would only make things worst, as your server would continually send data that would get cancelled, increasing it's load factor and slow it down. Also, consider this:
if your user scroll frenetically up and down the page, he/she will expect some loading delays.
having a timeout delay (ex: 200 ms) before starting to load a portion of the page is pretty acceptable, and how many times will one stop and jump after 200 ms interval on your page? Even it it happens, it comes back to point 1
how big are your images? Even a slow server can serve about a few tens of 3Kb thunbnails per second. If your site has bigger images, consider using low and hi resolution images with some components like lightBox
Often, computer problems are simply design problems.
** EDIT **
Here's an idea :
your page should display DIV containers with the width and height of the expected image size (use CSS to style). Inside of each DIV, add an link. For example :
<div class="img-wrapper thumbnail">
Loading...
</div>
Add this Javascript (untested, the idea is self describing)
$(function() {
var imgStack;
var loadTimeout;
$(window).scroll(function() {
imgStack = null;
if (loadTimeout) clearTimeout(loadTimeout);
loadTimeout = setTimeout(function() {
// get all links visible in the view port
// should be an array or jQuery object
imgStack = ...
loadNextImage();
}, 200); // 200 ms delay
});
function loadNextImage() {
if (imgStack && imgStack.length) {
var nextLink = $(imgStack.pop()); // get next image element
$('<img />').attr('src', nextLink.attr('href'))
.appendTo(nextLink.parent())
.load(function() {
loadNextImage();
});
// remove link from container (so we don't precess it twice)
nextLink.remove();
}
};
});
Well, my idea:
1) initiate an AJAX request for the image, if it succeeds, the image goes to the browser cache, and once you set the 'src' attribute, the image is shown from the cache
2) you can abort the XHR
I wrote a tiny server with express emulating the huge image download (it actually just waits 20 seconds, then returns an image). Then I have this in my HTML:
<!DOCTYPE html>
<html>
<head>
<style>
img {
width: 469px;
height: 428px;
background-color: #CCC;
border: 1px solid #999;
}
</style>
</head>
<body>
<img data-src="./img" src="" />
<br />
<a id="cancel" href="javascript:void(0)">CANCEL</a>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script>
<script>
$(function () {
var xhr, img = $('img'), src = img.data('src');
xhr = $.ajax(src, {
success: function (data) { img.attr('src', src) }
});
$('#cancel').click(function (){
xhr.abort();
})
});
</script>
</body>
</html>
You can load your images using ajax calls, and in case that the uses scrolls-out, you can abort the calls.
In jQuery pseudo-code it would be something like that (forgive me mistakes in syntax, it is just an example):
1) tag images that you want to load
$(".image").each( function(){
if ( is_in_visible_area(this) ) { // check [1] for example
$(this).addClass("load_me");
} else {
$(this).addClass("dont_load");
}
});
2) load images
ajax_requests = {};
$(".image.load_me").each( function(){
// load image
var a = $.ajax({
url: 'give_me_photos.php',
data: {img: photo_id},
success: function(html){
photo_by_id(photo_id), img.append(html);
}
});
ajax_requests[photo_id] = a;
});
3) cancel loading those out of the screen
for( id in ajax_requests ) {
if ( ! is_in_visible_area(id) ) {
ajax_requests[id].abort();
}
}
Of course, add also some checking if the image is already loaded (e.g. class "loaded")
[1]. Check if element is visible after scrolling
[2]. Abort Ajax requests using jQuery
BTW, another idea that might work:
1) create a new iframe
2) inside of the iframe have the script that starts loading the image, and once it's loaded, call the .parent's method
3) when in need, stop the iframe content loading using .stop on the iframe object
Use a stack to manage ajax requests (means you will have serial loading instead of parallel but it is worth it)
On scroll stop, wait for 300ms and then push all images inside view-area into stack
Every time a user scrolls check if a stack is running. (fyi - you can stop all requests to a particular url instead of killing all ajax calls. also you can use regex so it should not stop any other requests on the page)
If an existing stack is running - pop all the images that are in it except for the top most one.
On all ajax calls - bind beforeSend() event to remove that particular image from the stack
It is late right now, but we have done something very similar at work - if you need the detailed code let me know.
Cheers!
Maybe you could serve the image through a php script which would check a field in the the db (or better yet a memcached) that would indicate stop loading. the script would portion up the image into chunks and pause in between each chunk and check if the stop flag for the particular request is. If it is set you send the header with A 204 no content which as soon as the browser gets it will stop receiving.
This may be a bit over kill though.
The solution could be a webworker. a webworker can be terminated and with him the connection.
But there is a small problem that the webworker uses the limited connections of the browser so the application will be blocked.
Right now I'm working on a solution with serviceWorkers - they don't have a connection limit (I hope so)

How to show a spinner while loading an image via JavaScript

I'm currently working on a web application which has a page which displays a single chart (a .png image). On another part of this page there are a set of links which, when clicked, the entire page reloads and looks exactly the same as before except for the chart in the middle of the page.
What I want to do is when a link is clicked on a page just the chart on the page is changed. This will speed things up tremendously as the page is roughly 100kb large, and don't really want to reload the entire page just to display this.
I've been doing this via JavaScript, which works so far, using the following code
document.getElementById('chart').src = '/charts/10.png';
The problem is that when the user clicks on the link, it may take a couple of seconds before the chart changes. This makes the user think that their click hasn't done anything, or that the system is slow to respond.
What I want to happen is display a spinner / throbber / status indicator, in place of where the image is while it is loading, so when the user clicks the link they know at least the system has taken their input and is doing something about it.
I've tried a few suggestions, even using a psudo time out to show a spinner, and then flick back to the image.
A good suggestion I've had is to use the following
<img src="/charts/10.png" lowsrc="/spinner.gif"/>
Which would be ideal, except the spinner is significantly smaller than the chart which is being displayed.
Any other ideas?
I've used something like this to preload an image and then automatically call back to my javascript when the image is finished loading. You want to check complete before you setup the callback because the image may already be cached and it may not call your callback.
function PreloadImage(imgSrc, callback){
var objImagePreloader = new Image();
objImagePreloader.src = imgSrc;
if(objImagePreloader.complete){
callback();
objImagePreloader.onload=function(){};
}
else{
objImagePreloader.onload = function() {
callback();
// clear onLoad, IE behaves irratically with animated gifs otherwise
objImagePreloader.onload=function(){};
}
}
}
You could show a static image that gives the optical illusion of a spinny-wheel, like these.
Using the load() method of jQuery, it is easily possible to do something as soon as an image is loaded:
$('img.example').load(function() {
$('#spinner').fadeOut();
});
See: http://api.jquery.com/load-event/
Use the power of the setTimeout() function (More info) - this allows you set a timer to trigger a function call in the future, and calling it won't block execution of the current / other functions (async.).
Position a div containing the spinner above the chart image, with it's css display attribute set to none:
<div> <img src="spinner.gif" id="spinnerImg" style="display: none;" /></div>
The nbsp stop the div collapsing when the spinner is hidden. Without it, when you toggle display of the spinner, your layout will "twitch"
function chartOnClick() {
//How long to show the spinner for in ms (eg 3 seconds)
var spinnerShowTime = 3000
//Show the spinner
document.getElementById('spinnerImg').style.display = "";
//Change the chart src
document.getElementById('chart').src = '/charts/10.png';
//Set the timeout on the spinner
setTimeout("hideSpinner()", spinnerShowTime);
}
function hideSpinner() {
document.getElementById('spinnerImg').style.display = "none";
}
Use CSS to set the loading animation as a centered background-image for the image's container.
Then when loading the new large image, first set the src to a preloaded transparent 1 pixel gif.
e.g.
document.getElementById('mainimg').src = '/images/1pix.gif';
document.getElementById('mainimg').src = '/images/large_image.jpg';
While the large_image.jpg is loading, the background will show through the 1pix transparent gif.
Building on Ed's answer, I would prefer to see something like:
function PreLoadImage( srcURL, callback, errorCallback ) {
var thePic = new Image();
thePic.onload = function() {
callback();
thePic.onload = function(){};
}
thePic.onerror = function() {
errorCallback();
}
thePic.src = srcURL;
}
Your callback can display the image in its proper place and dispose/hide of a spinner, and the errorCallback prevents your page from "beachballing". All event driven, no timers or polling, plus you don't have to add the additional if statements to check if the image completed loading while you where setting up your events - since they're set up beforehand they'll trigger regardless of how quickly the images loads.
Some time ago I have written a jQuery plugin which handles displaying a spinner automatically http://denysonique.github.com/imgPreload/
Looking in to its source code should help you with detecting when to display the spinner and with displaying it in the centre of the loaded image.
I like #duddle's jquery method but find that load() isn't always called (such as when the image is retrieved from cache in IE). I use this version instead:
$('img.example').one('load', function() {
$('#spinner').remove();
}).each(function() {
if(this.complete) {
$(this).trigger('load');
}
});
This calls load at most one time and immediately if it's already completed loading.
put the spinner in a div the same size as the chart, you know the height and width so you can use relative positioning to center it correctly.
Aside from the lowsrc option, I've also used a background-image on the img's container.
Be aware that the callback function is also called if the image src doesn't exist (http 404 error). To avoid this you can check the width of the image, like:
if(this.width == 0) return false;
#iAn's solution looks good to me. The only thing I'd change is instead of using setTimeout, I'd try and hook into the images 'Load' event. This way, if the image takes longer than 3 seconds to download, you'll still get the spinner.
On the other hand, if it takes less time to download, you'll get the spinner for less than 3 seconds.
I would add some random digits to avoid the browser cache.

Categories