[翻译]只为图片使用IMG标签(Use IMG tags only for Images)

原文地址:Use IMG tags only for Images

首先,补充一些背景知识。

web开发人员经常通过在主页预加载(预缓存)将来的页面所用到的一些资源的方式来优化网站的性能。常用的手段是在主页内容加载完之后开启预读取资源下载。有些网站通过使用IMG标签的形式来打到预读取的目的----IMG没有同源的限制(limited to same-origin requests),这使它看起来是一种理想的选择。如果通过IMG标签来预读取css文件或者js文件,这个文件将只会被加载,但不会被解析和执行。

在有些网站中,尤其是一些前沿(cutting-edge )网站中,他们尝试通过在页面很靠前的位置使用隐藏的IMG标签指向同一个页面后面将会使用的资源的方式来“帮助”浏览器的先行下载(Lookahead Downloaders)。通过这种方式,这些资源可以优先下载,当页面解析执行到真正使用这些资源的地方时,这些资源就可以在缓存中直接读取。这种方式实际上就是让浏览器早点进行资源下载。当这种方式必须要在页面的最开始(接近最开始,head,或者接近head的地方)使用,而且应该距离使用这些资源的script、link标签很远才能达到与读取的目的。

不幸的是,使用这种方式来预读取js和css文件实际上会拖慢整个页面。

我深爱着一个神秘的工具(Fiddler),因此当一个网站拥有者邮件告诉我,他使用Fiddler的时候发现,IE浏览器在同一个页面加载同一个资源两次,尽管第二个下载是在第一次下载完成之后才开始,而且第一次下载的资源已经带着一个可以让资源复用的设置了缓存的响应头时我会很兴奋。这到底是怎么回事呢?

幸好,他给我的信息是在 X-Download-Initiator header开启的情况下捕捉的,这样的话我就可以看到每次请求是如何产生的。第一次的脚本下载是在先行下载(浏览器自身策略)到达IMG标签的时候开始的。第二个请求是解析到使用同一个src的script标签时开始的。这两个URL是相同的,那为什么第二次请求还是会发送呢?

经过更深入的查看发现,原来第一个请求实际上是被浏览器取消了!这也解释了第二个请求会发送的原因(第一个请求根本没发出去,资源没有被加载)。为什么第一个请求会被取消呢?

当IE遇到IMG标签时,它就会创建一个image对象,并且分配给一个下载请求。当请求的资源数据开始到达时,数据就会流入浏览器的图片解码器。如果数据是文本的话,解码器就会把数据当成异常数据并拒绝执行。这看上去很是合理:解码器不可能利用这些数据。当解码器把数据当成“不可能是图片(Not possibly an image)”的异常时,前面创建的image对象就会取消它的过程。如果资源还没有下载完毕,也会被当成过程的一部分被取消。

取消一个下载对于性能非常不利。首先,浏览器不会得到请求的资源----只有在取消前已经下载完的部分资源可以被缓存,甚至这一部分只有在响应中带上ETAG和content-length信息的时候才能被缓存。如果这两个response头信息都有的话,浏览器才可能在后面通过HTTP Range request请求下载资源的剩余部分。   其次,建立TCP/IP连接是非常昂贵的(也许在这前面还有HTTPS的握手过程),抛弃建立好的连接会明显的增加页面的加载时间。

我制作了一个叫MeddlerScript 的小工具,这个工具可以清楚地展示这个问题。在 Meddler中加载这个ms文件,然后点击 Meddler上的编译和查看按钮。你的浏览器将会打开一个会加载两个js文件的html页面,一个用于IMG标签,另一个用于script标签。script标签使用的js资源请求会像预期的那样立即执行,而IMG标签使用的JS资源请I去也会像预期的那样不会执行。如果你点击第二个HTML页面上的链接,script标签拉取的脚本会直接从缓存中读取和使用,而IMG标签拉取的脚本则会从服务器重新下载。查看Meddler的log,会看到如下信息:

Script loaded at 7:15:26.  Ready for connections. 
0: GET /; 
1: GET /796-SCRIPTPretendingToBeAImg.js; 
2: GET /796-SCRIPTNOTPretendingToBeAImg.js; 
1: Error: An established connection was aborted by the software in your host machine 
3: GET /UseTheScript.htm; 
4: GET /796-SCRIPTPretendingToBeAImg.js; Range: bytes=4237-; If-Range: "796";

黄底行显示通过IMG标签拉取JS资源的下载会被取消和关闭TCP连接。然后在session#4中可以看到,在第二个页面中,浏览器会发送一个HTTP请求去获取脚本的剩余数据。

如果使用Fiddler模拟这个脚本,必须开启Streaming Mode才能看到log信息。这是因为Fiddler默认会完全缓冲每个响应,并交付给浏览器一个快照,这意味着image对象不可能被取消,除非下载已经完成和缓存。这也重申了通过IMG预读取资源会成功的情况只发生在网络状况很好的时候。

相同的取消行为发生在IE6-IE10和Firefox 12.0中,Opera 11.61, Chrome 18, 和Safari 5.1.5看起来在image内容不合法的情况下也不会取消请求。

有意思的是,Firefox好像也不会缓存部分的响应数据;当脚本再次下载的时候,请求不会带上Range头。这种行为也许可以解释,如果Firefox对于图片请求和其他标签采取分离的缓存(某种我相信Mozilla在一些地方使用的缓存结构)。这个观点(缓存分离)还有更深入的证据支持。如果更新(刷新)MeddlerScript让第一个请求可以快速的完成,让浏览器根本没机会取消,Firefox还是会在第二个页面中重新下载脚本文件。

Script loaded at 7:31:39.  Ready for connections. 
0: GET /; 
1: GET /143-SCRIPTPretendingToBeAImg.js; 
2: GET /143-SCRIPTNOTPretendingToBeAImg.js; 
3: GET /usethescript.htm; 
4: GET /143-SCRIPTPretendingToBeAImg.js;

当web开发者遇到这个问题来请教我有没有替代方案时,我首先想到的是尝试使用IE的startDownload方法来容纳他们的脚本,即使这个方法有同源策略的限制。不幸的是,startDownload方法并不是一个合适的替代品。通过startDownload开启的下载的创建是包含no-cache的,就会导致创建请求时缓存是不会被读取的,而且响应也不会被缓存。

HTML5提供了一个明确的方式(prefetch Link relation)允许浏览器预读取资源。IE9以上(注:chrome、firefox也都支持)支持通过link来进行DNS-prefetching(DNS预读取);这些资源并不会被下载。

-Eric

PS:通过IMG标签来预读取图片当然是没有问题的......只要你不是在HTTPS页面上请求HTTP域的图片。这样做的话(在HTTPS页面上请求HTTP的图片)会引起内容错乱的问题( Mixed-Content problem),而且你页面的锁定icon将会消失。

(文章后面有几条评论也很不错,一起翻译了)

igrigorik(web性能权威指南的作者) 
5 May 2012 2:40 PM

Eric, 很棒的深入研究! 有点好奇, IE对于在object标签上的类似情况(通过object加载css、js资源)是如何处理的? 理论上来说,通过object标签做预读取应该可以避免取消(终止)解析。说了这么多,向你指出的那样, 预读取hack就是......(不可能实现)。 我希望有些事我们可以远离(something I hope we can migrate away from)。

EricLaw [ex-MSFT] 
7 May 2012 11:24 AM

@igrigorik: 看起来我不能得到一个不会引起相同问题的OBJECT标签(最少在标准模式下)。可能有些参数的组合可以起到作用,但我不知道是哪种组合。

Mathieu 'p01' Henri 
20 May 2012 6:33 AM

通过IMG标签来实现这种方式真的很不好。我更倾向于使用object标签。

@EricLaw, 你试过<object type="text/plain" data="scriptNeededLater.js" ></object> 吗? 这种方式不需要伪装就可以用于各种资源。这可以看作是一种通用内容类型的同一资源。

EricLaw [ex-MSFT]

20 May 2012 6:43 AM

@Mathieu: 你可以修改MeddlerScript来测试这种方式. 不幸的是,你会发现第一个页面只产生了一个HEAD 请求, 然后确定了object的MIME类型不是准备渲染的类型,然后就停止了。你应该想的更远一些,确保资源自身是text/plain的内容类型,但及时确保text/plain内容类型有用也不建议使用,因为(当从服务端返回的头部信息中设置了X-Content-Type-Options: nosniff,且请求的url是通过script标签引用的方式时,如果返回的Content-Type类型不是以下之一["text/javascript", "application/javascript", "text/ecmascript", "application/ecmascript", "text/x-javascript", "application/x-javascript", "text/jscript", "text/vbscript", "text/vbs"]的话,IE9下是不会加载请求的js文件的。参考文章:通过动态添加script标签请求数据(JSONP)的一些问题)如果响应头部信息包括X-Content-Type-Options: nosniff指令时整个过程就会中断。而且还会造成缓存清理的混乱(js相对于其他类型被清理的情况要轻得多)。

chrisbro

22 Jan 2014 3:26 PM

Eric, 我对于你在igrigorik的回复中的否定抱有疑问。 你的回答应该可以理解为 我也不能让Object标签起作用",是吧?  我们通过使用Mathieu说的<object type="text/plain" data="scriptNeededLater.js" ></object>做了一个页面,这个页面会预加载js文件(后面的独立页面会用到)。IE10会做HEAD请求,IE10也会发起get请求----但是Fiddler显示这个请求被“取消”了,而且这个资源并没有实际缓存起来。很难判断IE10到底下载了几个文件。

EricLaw [ex-MSFT]

22 Jan 2014 3:37 PM

我直接下结论吧: 不要使用Object标签,这个是没用的。 对于IE11+(chrome、firefox), 使用<link rel="prefetch">.

Radko Dinev

6 Feb 2014 5:29 AM

@EricLaw: 如你所说,如果头部信息包含Content-Length和ETag,即使是部分数据也会被缓存。如果用Last-Modified代替ETag呢, 效果是不是也应该是一样的呢?

[EricLaw]: 至少在IE9, 继续请求下载部分数据后的剩余数据需要相应中包含ETAG(requires that the response contain a strong ETAG);只包含Last-Modified date的响应是不会被继续获取的。

后记:本来想在业务中使用controljs的,但是看到这篇文章,再想到IE的占有份额,谁还敢用?相对于资源下载,还是TCP连接保持更重要。不过对于不同域的资源请求使用这个方法也是一种提速方案。还有,这篇文章只提到了js的问题,CSS呢?有时间的话自己研究一下。

参考文章:

Thoughts on script loaders

preload css javascript without execution

ControlJS介绍

用ControlJS优化阿里妈妈广告

http://stevesouders.com/controljs/

https://groups.google.com/forum/#!forum/controljs

上一篇:阿里云+wordpress


下一篇:Oracle组函数、多表查询、集合运算、数据库对象(序列、视图、约束、索引、同义词)等