【问题标题】:Image preload and sequential display in JavaScriptJavaScript 中的图像预加载和顺序显示
【发布时间】:2019-12-11 23:06:53
【问题描述】:

我想要的是以下内容:

  1. 预加载图像(最多 40-50 个),并在完成后进行回调(例如警报)(我认为这部分已经很好)
  2. 要点:在同一个div中逐张显示图片,尽可能精确地记录显示时间(同时任何图片都可以显示多次)
  3. 在另一个 div 中显示相同的图像(如果我有第 2 点,这应该相当容易)

更多背景信息:这是一个响应时间实验,在线参与者看到图片并对其做出关键响应(并测量他们的响应时间)。 (另外,显示之间至少有200-500毫秒的时间,所以每次显示前准备一点时间也没问题。)

我已经有一个完整的工作代码,这是我从以前的各种答案中整理出来的:有人可以验证我的代码是否有意义,并且我没有其他办法可以提高它的效率吗?

让我感到困惑的一件事是“appendChild”在浏览器中的确切作用。 (换句话说:我知道该元素将被添加到页面中,但我不知道这对浏览器意味着什么。)目前我在实际显示之前将图像附加一点(100 毫秒) ,然后我通过将不透明度设置为 0 到 1 (which is presumed to be the most optimal method for precise timing) 来实现。然后在显示下一张图像之前删除图像(清空div)。但我想知道这样做是否有任何意义。例如,如果我在“预加载阶段”中附加了所有图像,并且在不需要它们时将它们的不透明度设置为 0,该怎么办?这对性能有何影响?或者,如果(尽管我不想这样做)我在不透明度更改之前附加它们怎么办? “附加”是否需要任何时间,还是会以任何方式影响即将到来的不透明度更改的时间? (编辑:现在我已经有了主要问题的答案,但如果能得到关于这一点的解释仍然会很好。)

明显的问题是我不能真正准确地测量显示时间(没有外部硬件),所以我必须依靠“似乎有意义”的东西。

无论如何,下面是我的代码。

var preload = ['https://www.gstatic.com/webp/gallery/1.jpg', 'https://www.gstatic.com/webp/gallery3/1.png', 'https://www.gstatic.com/webp/gallery/4.jpg', 'https://www.gstatic.com/webp/gallery3/5.png'];
var promises = [];
var images = {};
// the function below preloads images; its completion is detected by the function at the end of this script
for (var i = 0; i < preload.length; i++) {
  (function(url, promise) {
    var filename = url.split('/').pop().split('#')[0].split('?')[0];
    images[filename] = new Image();
    images[filename].id = filename; // i need an id to later change its opacity
    images[filename].style.opacity = 0;
    images[filename].style.willChange = 'opacity';
    images[filename].style['max-height'] = '15%';
    images[filename].style['max-width'] = '15%';
    images[filename].onload = function() {
      promise.resolve();
    };
    images[filename].src = url;
  })(preload[i], promises[i] = $.Deferred());
}

// the function below does the actual display
function image_display(img_name, current_div) {
  window.warmup_needed = true;
  document.getElementById(current_div).innerHTML = ''; // remove previous images
  document.getElementById(current_div).appendChild(images[img_name]); // append new image (invisible; opacity == 0)
  chromeWorkaroundLoop(); // part of the timing mechanism
  setTimeout(function() {
    document.getElementById(img_name).style.opacity = 1; // HERE i make the image visible
    requestPostAnimationFrame(function() {
      window.stim_start = now(); // HERE i catch the time of display (image painted on screen)
      warmup_needed = false;
    });
  }, 100); // time needed for raF timing "warmup"
}


// below are functions for precise timing; see https://stackoverflow.com/questions/50895206/
function monkeyPatchRequestPostAnimationFrame() {
  const channel = new MessageChannel();
  const callbacks = [];
  let timestamp = 0;
  let called = false;
  channel.port2.onmessage = e => {
    called = false;
    const toCall = callbacks.slice();
    callbacks.length = 0;
    toCall.forEach(fn => {
      try {
        fn(timestamp);
      } catch (e) {}
    });
  };
  window.requestPostAnimationFrame = function(callback) {
    if (typeof callback !== 'function') {
      throw new TypeError('Argument 1 is not callable');
    }
    callbacks.push(callback);
    if (!called) {
      requestAnimationFrame((time) => {
        timestamp = time;
        channel.port1.postMessage('');
      });
      called = true;
    }
  };
}
if (typeof requestPostAnimationFrame !== 'function') {
  monkeyPatchRequestPostAnimationFrame();
}

function chromeWorkaroundLoop() {
  if (warmup_needed) {
    requestAnimationFrame(chromeWorkaroundLoop);
  }
}



// below i execute some example displays after preloading is complete

$.when.apply($, promises).done(function() {
  console.log("All images ready!");
  // now i can display images
  // e.g.:
  image_display('1.jpg', 'my_div');

  // then, e.g. 1 sec later another one

  setTimeout(function() {
    image_display('5.png', 'my_div');
  }, 1000);

});
<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js"></script>
<div id='my_div'></div>

【问题讨论】:

    标签: javascript image performance timing preload


    【解决方案1】:

    一个问题是,官方onload 只告诉我们网络状态(+元数据解码)。

    浏览器可能还需要执行一些其他操作才能将其呈现在屏幕上,例如将其完全解码为可移动到 GPU 的位图,以便最终呈现到屏幕上。

    至少在 Firefox 中可以看到这一点,方法是使用同步 drawImage 方法在画布上绘制图像;对于大图像,即使在您的代码运行之后,这个drawImage() 调用也可能需要几毫秒,这表明他们还没有完全解码它:

    var preload = ['https://upload.wikimedia.org/wikipedia/commons/c/cf/Black_hole_-_Messier_87.jpg'];
    var promises = [];
    var images = {};
    // the function below preloads images; its completion is detected by the function at the end of this script
    for (var i = 0; i < preload.length; i++) {
      (function(url, promise) {
        var filename = url.split('/').pop().split('#')[0].split('?')[0];
        images[filename] = new Image();
        images[filename].id = filename; // i need an id to later change its opacity
        images[filename].style.opacity = 0;
        images[filename].style.willChange = 'opacity';
        images[filename].style['max-height'] = '15%';
        images[filename].style['max-width'] = '15%';
        images[filename].onload = function() {
          promise.resolve();
        };
        images[filename].onerror = promise.reject;
        images[filename].src = url;
      })(preload[i] + '?r='+Math.random(), promises[i] = $.Deferred());
    }
    
    // the function below does the actual display
    function image_display(img_name, current_div) {
      window.warmup_needed = true;
      document.getElementById(current_div).innerHTML = ''; // remove previous images
      document.getElementById(current_div).appendChild(images[img_name]); // append new image (invisible; opacity == 0)
      chromeWorkaroundLoop(); // part of the timing mechanism
      setTimeout(function() {
        document.getElementById(img_name).style.opacity = 1;
        const c = document.createElement('canvas');
        c.width = document.getElementById(img_name).width
        c.height = document.getElementById(img_name).height;
        console.time('paint1');    c.getContext("2d").drawImage(document.getElementById(img_name),0,0);
        console.timeEnd('paint1');
    
        console.time('paint2');    c.getContext("2d").drawImage(document.getElementById(img_name),0,0);
        console.timeEnd('paint2');
    
        // HERE i make the image visible
        requestPostAnimationFrame(function() {
          window.stim_start = now(); // HERE i catch the time of display (image painted on screen)
          warmup_needed = false;
        });
      }, 100); // time needed for raF timing "warmup"
    }
    
    
    // below are functions for precise timing; see https://stackoverflow.com/questions/50895206/
    function monkeyPatchRequestPostAnimationFrame() {
      const channel = new MessageChannel();
      const callbacks = [];
      let timestamp = 0;
      let called = false;
      channel.port2.onmessage = e => {
        called = false;
        const toCall = callbacks.slice();
        callbacks.length = 0;
        toCall.forEach(fn => {
          try {
            fn(timestamp);
          } catch (e) {}
        });
      };
      window.requestPostAnimationFrame = function(callback) {
        if (typeof callback !== 'function') {
          throw new TypeError('Argument 1 is not callable');
        }
        callbacks.push(callback);
        if (!called) {
          requestAnimationFrame((time) => {
            timestamp = time;
            channel.port1.postMessage('');
          });
          called = true;
        }
      };
    }
    if (typeof requestPostAnimationFrame !== 'function') {
      monkeyPatchRequestPostAnimationFrame();
    }
    
    function chromeWorkaroundLoop() {
      if (warmup_needed) {
        requestAnimationFrame(chromeWorkaroundLoop);
      }
    }
    
    
    
    // below i execute some example displays after preloading is complete
    
    $.when.apply($, promises).done(function() {
      console.log("All images ready!");
      // now i can display images
      // e.g.:
      image_display('Black_hole_-_Messier_87.jpg', 'my_div');
    }).catch(console.error);
    <h4>run this snippet in Firefox</h4>
    <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js"></script>
    <div id='my_div'></div>

    在我的 FF 我得到 ​​p>

    所有图片都准备好了!
    油漆 1:665.000 毫秒
    油漆2:0.000ms

    这意味着对于这个巨大的图像,在你开始测量我的响应时间后,屏幕会空着 650 毫秒。


    HTMLImageElement 接口中添加了一个.decode() 方法,这应该可以让我们更接近绘画时间,但如果你没看错这个答案的介绍,我们已经找到了更好的解决方案:

    CanvasRenderingContext2D.drawImage() 是同步的。

    而不是呈现不同的&lt;img&gt;,冒着整个页面重排的风险,触发 CSS 过渡或其他可能延迟图像显示的东西,保持低级,使用单个可见 DOM 元素,并使用同步绘画方法.换句话说,使用 HTMLCanvasElement

    var canvas = document.getElementById('mycanvas');
    var ctx = canvas.getContext('2d');
    
    // modified version of image_display function
    function image_display(img_name, current_div) {
      window.warmup_needed = true;
      chromeWorkaroundLoop(); // part of the timing mechanism
      setTimeout(function() {
        // clear previous
        ctx.clearRect( 0, 0, canvas.width, canvas.height );
        // request the painting to canvas
        // synchronous decoding + conversion to bitmap
        var img = images[img_name];
        // probably better to use a fixed sized canvas
        // but if you wish you could also change its width and height attrs
        var ratio = img.naturalHeight / img.naturalWidth;
        ctx.drawImage( img, 0, 0, canvas.width, canvas.width * ratio );
        // at this point it's been painted to the canvas,
        // remains only passing it to the screen
        requestPostAnimationFrame(function() {
          window.stim_start = performance.now();
          warmup_needed = false;
        });
      }, 100); // time needed for raF timing "warmup"
    }
    
    
    // preloading is kept the same
    var preload = ['https://www.gstatic.com/webp/gallery/1.jpg', 'https://www.gstatic.com/webp/gallery3/1.png', 'https://www.gstatic.com/webp/gallery/4.jpg', 'https://www.gstatic.com/webp/gallery3/5.png'];
    var promises = [];
    var images = {};
    // the function below preloads images; its completion is detected by the function at the end of this script
    for (var i = 0; i < preload.length; i++) {
      (function(url, promise) {
        var filename = url.split('/').pop().split('#')[0].split('?')[0];
        images[filename] = new Image();
        images[filename].id = filename; // i need an id to later change its opacity
        images[filename].style.opacity = 0;
        images[filename].style.willChange = 'opacity';
        images[filename].style['max-height'] = '15%';
        images[filename].style['max-width'] = '15%';
        images[filename].onload = function() {
          promise.resolve();
        };
        images[filename].src = url;
      })(preload[i], promises[i] = $.Deferred());
    }
    
    
    
    
    // below are functions for precise timing; see https://stackoverflow.com/questions/50895206/
    function monkeyPatchRequestPostAnimationFrame() {
      const channel = new MessageChannel();
      const callbacks = [];
      let timestamp = 0;
      let called = false;
      channel.port2.onmessage = e => {
        called = false;
        const toCall = callbacks.slice();
        callbacks.length = 0;
        toCall.forEach(fn => {
          try {
            fn(timestamp);
          } catch (e) {}
        });
      };
      window.requestPostAnimationFrame = function(callback) {
        if (typeof callback !== 'function') {
          throw new TypeError('Argument 1 is not callable');
        }
        callbacks.push(callback);
        if (!called) {
          requestAnimationFrame((time) => {
            timestamp = time;
            channel.port1.postMessage('');
          });
          called = true;
        }
      };
    }
    if (typeof requestPostAnimationFrame !== 'function') {
      monkeyPatchRequestPostAnimationFrame();
    }
    
    function chromeWorkaroundLoop() {
      if (warmup_needed) {
        requestAnimationFrame(chromeWorkaroundLoop);
      }
    }
    
    
    
    // below i execute some example displays after preloading is complete
    
    $.when.apply($, promises).done(function() {
      console.log("All images ready!");
      // now i can display images
      // e.g.:
      image_display('1.jpg', 'my_div');
    
      // then, e.g. 1 sec later another one
    
      setTimeout(function() {
        image_display('5.png', 'my_div');
      }, 1000);
    
    });
    canvas {
      max-width: 15%;
      max-height: 15%
    }
    <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js"></script>
    <canvas id="mycanvas" width="500" height="500"></canvas>

    现在请注意,网络浏览器远不是您想要做的事情的完美工具。他们更喜欢 UI 响应能力和低内存使用率,而不是响应时间的准确性。

    【讨论】:

    • 非常感谢另一个超级答案!是的,我当然知道它并不完美,但是当没有别的东西时它就足够了。无论如何,您的解决方案看起来不错,但我还有一个问题。我在您的代码中没有看到任何不透明度变化:这是否意味着我为图像提供的样式在您的解决方案中变得无关紧要?我从代码中删除了所有样式属性(images[filename]),一切似乎都一样。
    • 是的,我的 sn-p 根本不使用这些样式。如果您需要在某些时候不显示任何内容,请使用 ctx.clearRect 方法,就像我在 image_display 开始时所做的那样。关键是您可以完全控制显示的内容以及使用画布作为唯一渲染器的时间。
    猜你喜欢
    • 2016-06-22
    • 1970-01-01
    • 1970-01-01
    • 2013-10-21
    • 1970-01-01
    • 2013-07-27
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多