I was facing the same problem some time ago (when doing service for photographers, using angular).
Problem is not about RxJS or angular, it is rather about browser itself - it is not optimised for displaying lots of big images this way.
At first if you need to display lots of images(does not matter is it local or remote files):
- Resize them before displaying (faster loading, no need for resize, lower memory consumption).
- If you can - display only visible images (otherwise page would be really slow until all images would be loaded). Check this answer: How do I get the x and y positions of an element in an AngularJS directive originally
trackVisibility
was written to display images only when they become visible.
About displaying images from local files, things are even more complicated:
In your case you are loading files as data urls, and there is a problem: 70 images you mentioned for 3 mb each will consume at least 2.1 Gb of RAM(actually more, and obliviously will affect performance)
First recommendation is - if you can: do not use data urls, better use URL.createObjectURL and use URL.revokeObjectURL when you do not need it anymore.
Second: if you need just thumbnails - resize images locally(using canvas) before displaying them. There would be an issue with antialiasing, if it is important for you case - take a look at step-down technic described here: Html5 canvas drawImage: how to apply antialiasing And if you are supporting iOS - there can be a problem with canvas size limitation, so you will need to detect it somehow. (both issues were addressed in example below)
And the last one: if you need to create thumbnails for lots of images - do not do this at once, instead - schedule work pieces over event loop (otherwise browser would be not responsive while resizing images). And for better preformance: do this sequentially(not in parallel for all images), it may sound strange - but, it would be faster(due too lower memory consumption, and less disk reads at the same time).
In summary:
- Use
trackVisibility
directive mentioned above to display only visible images
- Do not use, data urls especially for big images.
- Create resized thumbnails, before displaying them
Libraries you may find useful for implementing this:
Rough code example about doing image thumbnails (most of code was copied from working project - so, it is expected to work. canvasToJpegBlob
and makeThumbnail
was written just now and was not tested, so there can be small mistakes):
function loadImage(imagePath) {
return Rx.Observable.create(function(observer) {
var img = new Image();
img.src = imagePath;
image.onload = function() {
observer.onNext(image);
observer.onCompleted();
}
image.onError = function(err) {
observer.onError(err);
}
});
}
// canvas edge cases detection
var maxDimm = 32000;
var ios5 = false, ios3 = false;
(function() {
if (navigator.userAgent.indexOf('MSIE') !== -1 || navigator.appVersion.indexOf('Trident/') > 0) {
maxDimm = 8000;
} else {
var canvas = document.createElement('canvas');
canvas.width = 1024 * 3;
canvas.height = 1025;
if (canvas.toDataURL('image/jpeg') === 'data:,') {
ios3 = true;
} else {
canvas = document.createElement('canvas');
canvas.width = 1024 * 5;
canvas.height = 1025;
if (canvas.toDataURL('image/jpeg') === 'data:,') {
ios5 = true;
}
}
}
}());
function stepDown(src, width, height) {
var
steps,
resultCanvas = document.createElement('canvas'),
srcWidth = src.width,
srcHeight = src.height,
context;
resultCanvas.width = width;
resultCanvas.height = height;
if ((srcWidth / width) > (srcHeight / height)) {
steps = Math.ceil(Math.log(srcWidth / width) / Math.log(2));
} else {
steps = Math.ceil(Math.log(srcHeight / height) / Math.log(2));
}
if (steps <= 1) {
context = resultCanvas.getContext('2d');
context.drawImage(src, 0, 0, width, height);
} else {
var tmpCanvas = document.createElement('canvas');
var
currentWidth = width * Math.pow(2, steps - 1),
currentHeight = height * Math.pow(2, steps - 1),
newWidth = currentWidth,
newHeight = currentHeight;
if (ios3 && currentWidth * currentHeight > 3 * 1024 * 1024) {
newHeight = 1024 * Math.sqrt(3 * srcHeight / srcWidth);
newWidth = newHeight * srcWidth / srcHeight;
} else {
if (ios5 && currentWidth * currentHeight > 5 * 1024 * 1024) {
newHeight = 1024 * Math.sqrt(5 * srcHeight / srcWidth);
newWidth = newHeight * srcWidth / srcHeight;
} else {
if (currentWidth > maxDimm || currentHeight > maxDimm) {
if (currentHeight > currentWidth) {
newHeight = maxDimm;
newWidth = maxDimm * currentWidth / currentHeight;
} else {
newWidth = maxDimm;
newHeight = maxDimm * currentWidth / currentHeight;
}
}
}
}
currentWidth = newWidth;
currentHeight = newHeight;
if ((currentWidth / width) > (currentHeight / height)) {
steps = Math.ceil(Math.log(currentWidth / width) / Math.log(2));
} else {
steps = Math.ceil(Math.log(currentHeight / height) / Math.log(2));
}
context = tmpCanvas.getContext('2d');
tmpCanvas.width = Math.ceil(currentWidth);
tmpCanvas.height = Math.ceil(currentHeight);
context.drawImage(src, 0, 0, srcWidth, srcHeight, 0, 0, currentWidth, currentHeight);
while (steps > 1) {
newWidth = currentWidth * 0.5;
newHeight = currentHeight * 0.5;
context.drawImage(tmpCanvas, 0, 0, currentWidth, currentHeight, 0, 0, newWidth, newHeight);
steps -= 1;
currentWidth = newWidth;
currentHeight = newHeight;
}
context = resultCanvas.getContext('2d');
context.drawImage(tmpCanvas, 0, 0, currentWidth, currentHeight, 0, 0, width, height);
}
return resultCanvas;
}
function canvasToJpegBlob(canvas) {
return Rx.Observable.create(function(observer) {
try {
canvas.toBlob(function(blob) {
observer.onNext(blob);
observer.onCompleted();
}, 'image/jpeg');
} catch (err) {
observer.onError(err);
}
});
}
function makeThumbnail(file) {
return Observable.defer(()=> {
const fileUrl = URL.createObjectURL(file);
return loadImage(fileUrl)
.map(image => {
const width = 200;
const height = image.height * width / image.width;
const thumbnailCanvas = stepDown(image, width, height);
URL.revokeObjectURL(fileUrl);
return thubnailCanvas;
})
.flatMap(canvasToJpegBlob)
.map(canvasBlob=>URL.createObjectURL(canvasBlob))
.map(thumbnailUrl => {
return {
file,
thumbnailUrl
}
})
});
}