如何在HTML / JavaScript中加快本地文件系统中大图像的显示速度

how to speed up display of large images from local file system in html / javascript

本文关键字:文件系统 图像 速度 显示 HTML JavaScript      更新时间:2023-09-26

我有一个html应用程序,我正在处理大量的大图像。我们谈论的可能是 5,000 张照片,每张照片大约 3-5MB。

到目前为止,我正在测试大约 1000 张图像,事情已经变得非常缓慢。

我正在使用拖放和文件阅读器来加载图像,然后将文件读取器结果设置为图像的来源:

    private loadImageFromDisk(image: IImage): Rx.Observable<IImage> {
        return Rx.Observable.defer( () => {
            console.log( `loading ${image.file.name} from disc` );
            console.time( `file ${image.file.name} loaded from file system` );
            const reader = new FileReader();
            setTimeout( () => reader.readAsDataURL(image.file), 0 ) ;
            const subject = new Rx.Subject();
            reader.onload = event => {
                subject.onNext( reader.result );
                subject.onCompleted();
            }
            return subject
                .safeApply(
                    this.$rootScope,
                    result => {
                        console.timeEnd( `file ${image.file.name} loaded from file system` );
                        image.content = reader.result;
                    }
                )
                .flatMap( result => Rx.Observable.return( image ) );
        } );
    }

.html:

        <div
            ng-repeat="photo in controller.pendingPhotos"
            class="mdl-card photo-frame mdl-card--border mdl-shadow--4dp">
            <div class="mdl-card__title">
                {{photo.file.name}}
            </div>
            <div class="img-placeholder mdl-card__media">
                <div
                    ng-if="!photo.content"
                    class="mdl-spinner mdl-js-spinner is-active"
                    mdl-upgrade
                    ></div>
                <img class="img-preview" ng-if="photo.content" ng-src="{{photo.content}}"/>
            </div>
            <div class="mdl-card__supporting-text" ng-if="photo.response">
                {{controller.formatResponse(photo.response)}}
            </div>
        </div>

我知道ng-repeat可能是一个性能问题,我会对此进行排序,但目前即使显示一张图像也可能需要几秒钟。如果我从光盘加载图像但实际上没有显示它,则从光盘加载每个图像只需要大约 50-100 毫秒。如果我显示它,事情会变慢得多。

我怀疑速度变慢是浏览器(铬)必须调整图像大小。

在我对 70 张图像进行的测试中,我将它们全部加载到浏览器中,在加载所有内容并渲染后,前几次我上下滚动页面时滚动性能很慢,之后它很流畅。

这些图像约为 3,000 x 2,000 像素。我正在将它们的大小调整为 200 像素长以显示它们。

加快

速度的最佳方法是什么?

前段时间我遇到了同样的问题(在为摄影师提供服务时,使用角度)。

问题不在于 RxJS 或角度,而在于浏览器本身 - 它没有针对以这种方式显示大量大图像进行优化。

首先,如果您需要显示大量图像(无论是本地文件还是远程文件):

  1. 在显示之前调整它们的大小(加载速度更快,无需调整大小,内存消耗更低)。
  2. 如果可以 - 仅显示可见图像(否则页面将非常慢,直到加载所有图像)。检查这个答案:如何在 AngularJS 指令中获取元素的 x 和 y 位置trackVisibility该指令最初编写为仅在图像可见时才显示图像。

关于显示本地文件中的图像,事情更加复杂:

在您的情况下,您将文件加载为数据 url,并且存在一个问题:您提到的 70 张图像,每张 3 mb 将消耗至少 2.1 Gb 的 RAM(实际上更多,并且会不知不觉地影响性能)

第一个建议是 - 如果可以的话:不要使用数据URL,最好使用URL.createObjectURL,并在不再需要时使用URL.revokeObjectURL。

第二:如果您只需要缩略图 - 在显示图像之前在本地(使用画布)调整图像大小。抗锯齿会有一个问题,如果它对你很重要 - 看看这里描述的降压技术:Html5 canvas drawImage:如何应用抗锯齿 如果你支持 iOS - 画布大小限制可能存在问题,所以你需要以某种方式检测它。(这两个问题都在下面的示例中得到解决)

最后一个:如果您需要为大量图像创建缩略图 - 不要一次执行此操作,而是通过事件循环安排工作(否则浏览器在调整图像大小时不会响应)。为了更好的性能:按顺序执行此操作(不是对所有图像并行),这听起来可能很奇怪 - 但是,它会更快(由于内存消耗太低,同时磁盘读取更少)。

总结:

  1. 使用上面提到的trackVisibility指令仅显示可见图像
  2. 不要使用,数据网址,特别是对于大图像。
  3. 在显示缩略图之前创建调整大小的缩略图

您可能会发现库对实现这一点很有用:

  • https://github.com/blueimp/JavaScript-Load-Image
  • https://github.com/blueimp/JavaScript-Canvas-to-Blob

关于做图像缩略图的粗略代码示例(大部分代码是从工作项目中复制的 - 所以,它应该工作。 canvasToJpegBlobmakeThumbnail是刚才写的,没有经过测试,所以可能会有小错误):

 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
         }
       })
   });
  }