预加载资源研究

什么是预加载

所谓预加载,就是通过一定的编程方法,使浏览器在空间的时候,在后台通过HTTP请求访问某些资源。当用户在一段时间后真正使用这些资源的时候,相比一个完整的(返回200)的请求,可以更快地获得这些资源(返回304或者直接命中浏览器缓存)。

预加载在部分情况下有着十分重要的意义,特别是当确定某些资源用户在短时间内会使用,如分页列表的上一页和下一页、以及一些常用的LOGO之类的图片等。

预加载资源可能的方式

预加载的原理就是想办法发送一个HTTP请求,对响应的缓存等都由浏览器完成,因此一切有可能读取远程资源的方案都可以成为预加载资源的方案,大致有以下几类:

常规方式

  • 使用script标签:<script type="other/prefetch" src="some.res"></script>
  • 使用img标签:<img src="some.res" />
  • 使用iframe标签:<iframe src="some.res"></iframe>
  • 使用XMLHttpRequest加载:$.get('some.res');
  • 使用Flash进行加载:需要编写特定的Flash

非常规方式

  • 使用背景图片:<div style="background-image: url(some.res);"></div>
  • 使用object或embed标签:<object type="other/prefetch" src="some.res"></object>
  • 使用link标签并修改media:<link rel="stylesheet" href="some.res" media="prefetch" />
  • 在CSS中做import:@import "some.res"

新一代方法

  • 使用Link Prefetch:<link rel="prefetch" href="some.res" />
  • 使用WebWorker:var worker = new Worker('some.res');
  • 使用@font-face:@font-face { font-family: prefetch; src: url(some.res); }

方法有很多,也可能会有更多,具体的使用方式就不详细说明了,具体需要注意的细节会在后文详细描述。

预加载资源方式的评估指标

每一种方式或多或少都有其长处和缺点,本次主要按以下几个维度进行评估:

[A]浏览器兼容性
主要考察包括IE6-8、Firefox3.5+、Chrome7+、Opera9+以及Safari4+的兼容性。
[B]资源位置的覆盖性
主要考察是否有跨域政策的限制,是否能读取第三方的资源。
[C]引入第三方资源的安全性
主要考察是否会对加载的资源进行解析和执行,是否可能产生如XSS等安全问题。
[D]引入资源类型的覆盖性
主要考察是否可以引入不同类型的资源,包括text、image、script、html等。
[E]是否可以确定何时完成预加载
由于预加载资源用时的不确定性,有可能导致用户在资源未加载完成时产生行为导致加载请求被中断。因此需要考察资源的加载完成是否可控,主要考察是否有load、error、readystatechange等事件。
[F]积累的标签的清理可行性
如果预加载资源的方法会引入多余的标签,如link、script等,需要考察在资源加载过程中,将对应的标签删除是否会导致请求中断。

评估中出现的问题

随着不断深入,各种方案的缺陷也被一点点挖掘,以下是一些不太容易注意到的奇怪的问题。

script标签

在Firefox下,当script标签的type属性是Firefox无法识别的脚本类型时,Firefox不会发送任何请求,基本上除了type="text/javascript"以外,Firefox都不予理睬。

img标签以及background-image

在Firefox下,当img标签或者background-image样式请求的内容返回的Content-Type不是image大类时,其响应体(Response Body)只会被接收1个包,其后的内容全部丢弃。

Flash

对于跨域的请求,Flash会先读取对方服务器上的策略xml文件,如策略文件允许跨域,则会进行加载。

object或embed标签

在Firefox下,无论元素是否隐藏,都会提示要求安装插件,当然这插件是找不到的。

在Chrome下,如果元素被隐藏,则不发起请求;元素未隐藏则提示安装插件。

在下载完插件前,所有浏览器都不会发起请求。

但如果object标签的type使用text/plain则不存在安装插件的问题,不过悲剧的是在IE下不会发出请求,而在Firefox和Chrome下会解析执行HTML资源

需要注意的是,object元素有其特殊性,创建一个object元素的代价远远大于其他元素。

link标签

在IE下有load事件,Opera中可以定时查看readystate以确定是否完成了请求,在其他浏览器中则不存在。

Link Prefetch

现阶段仅Firefox给予了支持,该功能在HTML5草案中,非常值得期待。

WebWorker

使用WebWorker加载的脚本文件会立刻被解析和执行,虽然在worker中的global对象是带有一定限制的,但依旧无法完全阻止第三方脚本注入有害的代码。

@font-face

@font-face仅在样式表中定义是不会发起请求的,必须创建一个元素,将其font-family设为该font-face,并且该元素必须被添加到DOM树中才会产生请求。

评估表格及基本判断

表格从前文所述的A-F共6个方面来考察各种预加载资源的方式,以期较为直观地去评价各种方式的优劣。

[A]浏览器兼容性    [B]是否跨域    [C]安全性    [D]资源类型    [E]是否有onload    [F]是否可以删除标签
*:只实测了DOM元素被删除后的效果,因无法控制GC,未测元素对象被GC的情况。
  [A] [B] [C] [D] [E] [F]*
script 1 1 1 0 1 1
img 0 1 1 0 1 1
iframe 1 1 0 1 1 1
XMLHttpRequest 1 0 1 1 1 1
Flash 1 0 1 1 1 1
背景图片 0 1 1 0 0 1
object/embed 0 1 0 0 1 1
link + media 1 1 1 1 0 1
@import 1 1 1 1 0 1
Link Prefetch 0 1 1 1 0 1
WebWorker 0 1 0 1 0 1
@font-face 0 1 1 1 0 1

从表格展现的数据来看,link+media的方式以及css @import方式都比较优秀,唯一的遗憾是无法获得其是否加载完成,因此需要站点自身通过研究用户的行为,保证用户2次操作的间隔足够完成资源的加载。

其他考虑

  • Link Prefetch作为HTML5草案中的标准,且浏览器底层级别支持,因此浏览器可以在带宽空余地时候才进行预加载,用户也可以通过一定的方式关闭该功能,各方面都有不俗的表现,如果可以推广到主流浏览器,应该是作为最值得推荐的方案。
  • @import需要服务器端辅助,将需要加载的资源打包成一个css格式的文件,文件中包含若干个@import声明,但是无论如何,浏览器会需要多一个请求用于加载这个动态生成的css文件。
  • link+media的方式中,可以通过修改link的href属性,使用一个link加载多个资源,不会因为标签过多导致DOM结构的臃肿以至于影响性能。
  • 在IE下,使用link+media的方式,无论下载来的内容是否被解析,IE会对页面进行一次redraw,这一次redraw的性能损失非常小,大致只是进入了redraw方法,但并没有真正地重新进行布局。
  • object标签必须与DOM树相连才会加载资源,使用object标签时,在非IE浏览器下,可以将object标签的宽和高均设为0,但IE不行。在IE中可以将宽高均设为1,并使用visibility: hidden; position: absolute; top: 0; z-index: -1000;的样式来将其隐藏。当然既然IE不会发起请求,就怎么设也没意义了……

结论?

根据使用场景的不同,不会有一种万能的最佳解决方案,但大致可以总结如下:

  • 加载同域资源,使用XMLHttpRequest即可。
  • 加载第三方可信任的资源,如同公司内不同系统,可以使用iframe加载非HTML资源。
  • 加载图片资源,使用img是最好的方式,但需要注意在请求过程中img不能被GC回收
  • 对于第三方的不可信任型资源,考虑使用link+media的方式加载,但无法确定加载完成的时间点。
  • 如果可以判断浏览器,针对Firefox使用Link Prefetch,针对IE使用img标签,其他浏览器使用script标签算是一种较为完美的解决方案。
  • 从未来看,Link Prefetch必将是大势所趋,大家给WHATWG提意见,让他加上onload吧。

参考资料

此条目发表在 Javascript, 浏览器 分类目录,贴了 , , , 标签。将固定链接加入收藏夹。

预加载资源研究》有 19 条评论

  1. Daniel 说:

    此文给力。受教了。

  2. PDU 说:

    很好的文章,给力

  3. Pingback 引用通告: 另类Ajax--基于Cookie的Ajax | FunnyHouse

  4. winting 说:

    你好,请教一下.
    预加载js文件 在IE7下会不会平行下载? 或者说他(下载的js)是不是也会阻塞页面的渲染?
    如何会的话, 一般位置是在哪里比较好?

    • int08h 说:

      先说结论:预加载js文件不会造成平等下载以及对页面渲染的阻塞

      我想首先应该明确一下预加载的意义:预加载的前提是HTTP协议级别本身带有缓存的支持,且协议级别的缓存不会因为文件的类型不同而产生不同的效果
      在此前提下,只要一个资源有一次完整的下载过程,且该过程中传递的HTTP响应头里有相关的缓存指令,则预加载一定是成功的
      因此,对资源预加载的方式的研究,最终是对任意类型的资源的下载方式的研究,而不会因为这个资源是js或者是css、图片等有特别的区分

      我之所以说上面的这些,是因为希望你明确一个概念:预加载js文件并不一定要使用script标签,任何能导致js文件被完整下载的方式都是可以的
      而IE6-7中会在下载期阻塞的,只有内联<script>标签所定义的资源,即以下情况下,一个js文件的下载是不会产生阻塞的:
      1、异步加载js,即标准的通过DOM API创建<script>标签,插入到DOM中
      2、使用非<script>标签的方式,如背景图片、@font-face、Link Prefetch、object、link元素等总总方式都是可行的

      而在此文中所列举的预加载的方式中,正好是没有内联<script>标签这种方式的,从一开始我们就已经将这种过于传统并存在着大量功能、性能上的隐患的方式剔除了,因此结论是预加载js文件并不会导致页面渲染的阻塞:)

  5. winting 说:

    谢谢,
    在IE7.0测试,应该是同一个域名下只能同时并发两个.
    在FF 下测试使用object 只是下载,很奇怪就是会多请求一次.
    不过有一个问题请教一下:
    高性能javascript编程说: UI thread 是 JavaScript 执行 跟 UI update共享进程,
    对于外部的加载js文件,使用动态脚本加载js文件, 下载跟解析就不会影响到 UI thread(其中这个UI Thread 跟 上面的那个UI Thread是同一个)?但是js执行还是会阻塞(跟JavaScript是单线程有关系),
    不知道这些要如何解析?

    • int08h 说:

      IE6和IE7对HTTP1.1有2个并发的请求:)
      再次澄清一下所谓预加载的意思:所谓预,即该资源在未来会使用到,但当前不会使用,所谓未来,可能是几秒后,可能是下一个页面,可能是下一个模块,甚至可能永远不用
      因此预加载的核心在于“加载”而非执行,更有甚者,绝大多数的预加载需求是不允许资源被执行的,因为任何资源的执行,如javascript的执行、CSS的加入,都会影响到页面的现状,而所谓加载,即明确当前并不需要该项资源,自然不能允许页面的现状受到资源加载的执行
      因此,我们一般不会使用

  6. winting 说:

    谢谢你的回答:
    有一个问题请教一下:
    高性能javascript编程说: UI thread 是 JavaScript 执行 跟 UI update共享进程,
    对于外部的加载js文件,使用动态脚本加载js文件, 下载跟解析就不会影响到 UI thread(其中这个UI Thread 跟 上面的那个UI Thread是同一个)?不知道 下载跟解析 是不是也包含在 UI thread?
    但是js执行还是会阻塞(跟JavaScript是单线程有关系), 不知道这些要如何解析?

    • int08h 说:

      解析、执行是包含在UI Thread的,而下载过程并不包含,即下载理应是一个异步的过程,因此如果仅仅是加载(即下载)资源,而阻止了浏览器对其进行解析和执行,自然不会有阻塞的影响
      如果你的目标是预加载一个js,那么不要使用动态script标签,因为这会造成你想要加载的js最终被执行,与预期不符:)

  7. winting 说:

    不好意思:这个跟预下载没有关系啦.
    我只是说如何没有使用动态脚本加载的话, 下载,解析,执行是包含在UI Thread?
    使用动态脚本的话,下载,解析是不是不会在UI Thread?
    你看一下这个:http://www.slideshare.net/nzakas/high-performance-javascript-2011

    • int08h 说:

      不好意思看来是我误解了……任何javascript的代码,只要有解析和加载,必定是在浏览器的UI Thread上处理,只有这样才能保证javascript自身单线程的特性

  8. winting 说:

    http://www.slideshare.net/nzakas/high-performance-javascript-2011
    这个里面好像介绍的不是这样的,所以有点奇怪.

    • int08h 说:

      从这个幻灯片上看,我的理解一直是有误的呢……确实解析因为和DOM以及其他的javascript执行环境无关,移出UI Thread,在全新的Thread上处理完全是可以的……

  9. winting 说:

    不过有一点,我觉得就是执行这一个步骤不能移出? 很奇怪?难道这个是怕影响到UI?

    • int08h 说:

      执行就比较难移出了,一但将执行和UI的更新放在2个队列里,那么假设同时有2段javascript在执行:
      脚本1:
      document.body.style.color = ‘red’;
      alert(document.body.style.color);
      脚本2:
      document.body.style.color = ‘blue’;

      那么由于异步的关系,很可能最终的顺序是这样:
      document.body.style.colo = ‘red’;
      document.body.style.color = ‘blue’;
      UI更新
      alert(document.body.style.color);
      得到的结果是blue

      这和我们单线程下javascript执行的方式完全不同,引入了变量同步和线程冲突,在现有的编程模型下是完全不允许的

  10. winting 说:

    那就是说:执行主要是符合 javascript单线程?
    还有这个 UI thread 是 JavaScript 执行 跟 UI update共享进程?
    Javascript执行这个是指 javascript单线程?
    UI update 这个主要是指什么?

  11. winting 说:

    UI Thread 通过UI Queue 队列执行javascript 跟 UI Updates.
    跟javascript引擎线程,界面渲染线程,浏览器事件触发线程 这些有什么关系?
    这个有点迷糊.

  12. Pingback 引用通告: labjs分析に入った | テクニカルブログ

  13. Pingback 引用通告: 管理javascirpt脚本加载库——LABjs(Loading and Blocking Javascript) | 疯の吟唱

发表评论

电子邮件地址不会被公开。 必填项已被标记为 *

*

您可以使用这些 HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>