流行的,或许并非最佳方式
网上的一个非正式调查展示了预加载一组图片的一个通用的标准方式。简化形式如下:
/* 'images' is an array with image metadata including a 'url' property */ for (var i = 0; i < images.length; ++i) { var img = new Image(); img.src = images[‘url’]; }
这段代码遍历包含图片元数据的对象数组,为每个对象创建一个HTML Image对象,并为src属性设置url。一旦Image对象设置了src值,浏览器就会向服务器发起请求,并缓存返回的图片。
需要注意的是,浏览器请求是异步的。也就是说这段代码会遍历数组,每张图片几乎同时发起请求,并不需要等待服务器返回结果后顺序发起请求。对于现代浏览器而言,这段代码尝试并行下载4~8个张图片(当然如果来自不同域,会更多)。
并行下载的好处
网站通常会有一些资源,浏览器必须先下载后才能显式页面。HTML本身,一两个CSS文档,一些小的图片元素,字体,偶尔会有一些不可避免的JavaScript文件(需要在页面可以绘制前执行)。每一个文件通常都相当小,但是每个请求与服务器的往返都会导致延迟开销。虽然这种延迟通常很小,每个文档的延迟都是毫秒级的,但是如果浏览器需要等待一个请求完成后才能发起另一个请求的话,这些毫秒将依次累加,并迅速增至秒级,用户必须要等待这么长时间后才能浏览页面内容。如果可以同时发起所有请求,那么整体而言延迟时间会降低为一次的往返时延,从而使页面加载时间减少几秒钟。这并不会加快每个文档的实际下载速度,你仍受特定带宽的限制,所以4~8个的并行请求使下载速度降低4~8倍。但是总体下载速度的确加快了,因为你避免了连续的延迟开销。因为浏览器必须在下载了关键元素后才能绘制页面,避免顺序延迟时间累加意味着更快的页面绘制。
对预加载图片而言并非为一件好事
并行下载对于初始页面元素是非常有用的,因为浏览器在渲染页面前必须要先下载这些元素。一个CSS文件先于另一个CSS下载对于浏览器而言并无差别,因为浏览器需要两个文件都要下载后才能做其他事情。gallery里的图片预加载并非这种情况,你可以足够自信的预测哪个文件需要优先下载,你需要优先处理它,即使预加载的总体时间会稍长。
我网站的gallery的分析数据非常直观。虽然从一张图片切换至另一张图片的方式有多种,缩略图和上一张与下一张链接,90%的点击是在下一张链接上。几乎所有情况下,页面加载完成后的最关键因素是gallery里的下一张图片文件。如果使用标准的预加载方式,你无法控制这张图片何时加载。浏览器尝试加载gallery里的每张图片,以浏览器的最大并发请求组。这种技术很好的减少了gallery的总体加载时间,但也意味着加载最重要图片(下一张图片)的时间显著增长,因为它需要与其他并发请求竞争带宽。在相对缓慢的1.5Mbps DSL连接情况下,加载一张350K的图片需要2秒钟,浏览者有可能必须要等到4~6张图片加载完成后才能看到这种图片。也就是说gallery里的下一张图片可见前有一个潜在的12~15s的等待时间。有利的一面是这4~6张其他图片现在会被缓存起来,但是让用户盯着加载图片12秒钟,我们可能已经失去了这些用户。如果你以非「宽度连接」的方式测试你的网站的话,你可能会惊讶的发现,你的预加载器使你的网站对一些用户变得更糟。
一种更好的预加载方式
一旦理解了浏览者的行为,就可以设计一个预加载器,为大多数浏览者提供更好的体验。因为我知道几乎所有的浏览者是顺序浏览我的gallery,那么对我而言最好的策略是以相同的顺序加载图片。加载所有图片的总体时间可能会稍微长一些,因为我们会导致延迟开销累加,但此时的整体加载时间相对于页面的初始加载时间,并不是如此重要,因为在用户可以使用网站前,并不需要加载完所有的图片。我们只需要加载用户现在想看到的图片,javaScript如下:
function preload(imageArray, index) { index = index || 0; if (imageArray && imageArray.length > index) { var img = new Image(); img.onload = function() { preload(imageArray, index + 1); } img.src = images[index][‘serving_url’]; } } /* images is an array with image metadata */ preload(images);
注意:代码已做简化,生产环境代码会针对不同设备请求不同尺寸图片,并且会考虑用户进入gallery时非第一张图片的场景。
处理数组中第一张图片(index 0),添加onload事件处理函数,然后请求图片。只有当这张图片加载完成后,才会调用onload事件处理函数,然后为下一张图片做相同的操作,直到所有图片加载完成。
下图为使用Chrome开发者工具模拟2Mbps连接,下载序列和时间的比较:
并行预加载
(模拟的2Mbps连接,总体时间:24.01s,大图版本)
串行预加载
(模拟的2Mbps连接,总体时间:25.44s,大图版本)
标准预加载器的总体加载时间节省了1.5s。但是在这个案例中这个并非最重要的统计项。每个图表的最顶行表示gallery内下张图片的加载时间。在相对缓慢的连接环境下,使用标准的预加载器,页面加载完成14.27秒后,用户才能查看这张图片,即使这张图片处于第一行,主页面加载完成后就立即开始下载,这是因为它需要与图中所示的一些其他文档共享带宽。而顺序加载这些图片,会全带宽下载,于1.46秒内完成,提速接近于1000%。对于我们最常见的使用情况,这是一个巨大的进步。
这种策略需要关注非典型浏览者行为会产生什么效果?如果浏览者是另外10%用户,即点击了上一张图片链接或使用缩率图随机顺序跳转,会怎么样?因为我们的图片加载器每次仅加载一张图片,仍有可能并行加载另一张,并没有显著的性能损失。响应用户请求的JavaScript可以简单的请求图片,就像通常方式一样。因为它可能没有已经预加载和已缓存,浏览器会请求它,与当前正在加载的图片一起加载,如同让这条新请求加塞。由于在特定时间内仅有一张其他图片下载,浏览者的等待时间只比正常的等待时间稍长一点。在几乎所有情况下,相比在浏览器正在处理的6个并发请求之上再添加一个额外请求而言,这样的体验会更好。
另外一个好处是,由于浏览器一次仅处理一张图片,界面处于可响应状态。在标准预加载版本中,浏览器尝试同时下载和处理半打大型图片文档,这会消耗CPU时间,使页面的动画元素直到下载完成后才能处理。
避免过早预加载
在用户请求一个页面和浏览器可渲染这个页面之间会发生很多事情。这段时间内,你的用户只能坐在哪里看着一个空白页面。如果你珍惜你的用户,你应努力缩短这个时间。要做到这一点,需要尽可能快的传输浏览器所需的渲染页面的最少数据。你的预加载器不应参与其中。
至少对我而言,预加载代码最好放在window onload处理函数内,如果使用jQuery的:
$(window).load(function() { /* Preload code goes here */ });
————————————————————————————-