为什么浏览器在这里效率低下,会发出两个请求?

13

我发现关于ajax和图像加载有一些奇怪的问题。假设您在页面上有一张图片,并且ajax请求相同的图片 - 人们会猜想ajax请求将命中浏览器缓存,或者至少只会发出一个请求,结果是图像传递到页面和想要读取/处理图像的脚本。

令人惊讶的是,我发现即使javascript等待整个页面加载完成,图像请求仍然会发出新的请求!这是Firefox和Chrome中已知的bug,还是jQuery ajax在做什么坏事?

在这里您可以看到问题,打开Fiddler或Wireshark并设置记录,然后单击“运行”:

<script src="http://code.jquery.com/jquery-1.11.1.min.js"></script>
<div id="something" style="background-image:url(http://jsfiddle.net/img/logo-white.png);">Hello</div>
<script>

jQuery(function($) {
    $(window).load(function() {
        $.get('http://jsfiddle.net/img/logo-white.png');
    })
});

</script>
请注意,在Firefox中会发出两个请求,均返回200-OK,并将整个图像两次发送回浏览器。在Chromium中,它至少可以正确地在第二个请求上获得304,而不是下载整个内容两次。
奇怪的是,IE11会下载整个图像两次,而似乎IE9会积极缓存并仅下载一次。
理想情况下,我希望ajax根本不会发出第二个请求,因为它正在请求完全相同的url。这种情况下,CSS和Ajax通常具有不同的缓存的原因是什么?是否浏览器在为CSS与Ajax请求使用不同的缓存存储?

15
这段内容请求两个 jQuery 的副本,原因是你在 fiddle 选项中指定了一个版本,在代码中又链接到另一个版本,这很有趣。 - BoltClock
你的 Ajax 可能没有从图像缓存中加载图像,因为它没有加载到图像对象中。 - mplungjan
@mplungjan 可能是这样,我注意到 $('div').load() 不会触发事件,但是 $('img').load() 会触发事件。你知道有没有解决方法吗?背景图片是否完全与 JavaScript 可访问的对象分离? - NoBugs
@BoltClock 哈哈,我刚刚注意到了,它是从一个测试文件中复制并调整的 - 不过这并不是问题的原因。 - NoBugs
8
你知道俗话说的...永远不会有太多的jQuery。 - BoltClock
显示剩余8条评论
9个回答

7

我使用的是最新版本的Google Chrome,它只发送了一个请求。但是在你的JSFIDDLE示例中,你加载了两次jQuery。第一次是通过style属性载入CSS,第二次是通过script标签载入代码。改进后:JSFIDDLE

<div id="something" style="background-image:url('http://jsfiddle.net/img/logo-white.png');">Hello</div>

<script>
    jQuery(window).load(function() {
        jQuery.get('http://jsfiddle.net/img/logo-white.png');
    });

    // or

    jQuery(function($) {
        jQuery.get('http://jsfiddle.net/img/logo-white.png');
    });
</script>

jQuery(function($) {...}会在DOM就绪时被调用,而jQuery(window).load(...);则是在DOM就绪且所有图像和其他资源都已加载完毕时被调用。将它们嵌套在一起没有意义,详情请参阅这里:window.onload vs $(document).ready()

当你使用CSS和JavaScript时,可以在Web检查器的Network选项卡中看到图像被加载了两次,第一次通过CSS加载,第二次通过JavaScript加载,第二次请求可能被缓存。

更新: 但是无论缓存与否,每个请求都会显示在此选项卡中。请参考以下示例:http://jsfiddle.net/95mnf9rm/4/。 其中有5个请求带有缓存的AJAX调用,另外5个没有缓存。总共有10个请求在“网络”选项卡中显示。 当你在CSS中两次使用相同的图像时,它只会请求一次。但如果你显式地发起一个AJAX调用,那么浏览器会进行AJAX调用,然后可能会被缓存或不被缓存,但它是被显式请求的,对吧?


4
可以的,该句话的意思是:“当然,在Web检查器的“网络”选项卡中,这个图像被加载了两次。第一次是通过你的CSS加载的,第二次是通过你的JavaScript加载的。”提问者已经知道了这一点,这也是引发问题的原因。问题是为什么会这样,因为没有明显的理由说明浏览器为什么会因为从两个不同的上下文请求相同的资源而加载两次。如果你说Google Chrome只会发送一个请求,那么你能解释一下这种差异和Web Inspector中看到的情况吗? - BoltClock
1
啊,现在我完全明白了:你的意思是为什么它没有被缓存或类似的东西。 - algorhythm
1
它们被缓存了,但请求也显示在“网络”选项卡中。在以下JSFIDDLE中有10个AJAX请求,前5个具有缓存“true”,而其他请求则没有缓存。前5个请求非常快,而其他请求则很慢。但是这10个请求都在“网络”选项卡中。http://jsfiddle.net/95mnf9rm/4/ - algorhythm
是的,这就是我想要解释的。有两个请求是正确的。第一个是CSS,第二个是你明确完成的Ajax调用。当你进行get时,浏览器会连接服务器并获取文件,而不会缓存或类似的操作。 - algorhythm
@LivingSoftware 我从来没有说过相反的话,请看我的回答中几天前的更新... - algorhythm
显示剩余7条评论

4

这个“问题”可能是一个CORS预检请求

我一段时间前在我的应用程序中注意到了这个问题,从单页应用程序中检索信息的调用会进行两次。只有当您访问不同域上的URL时才会发生这种情况。在我的情况下,我们在与我们构建的应用程序所在不同的服务器上构建和使用API。我注意到,当我在我的应用程序中使用GET或POST访问这些 RESTFUL API 时,似乎会进行两次调用。

正在发生的事情称为预检请求https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS),它会向服务器发送初始请求以查看随后的调用是否允许。

摘自MDN:

与简单请求不同,“预检请求”首先通过OPTIONS方法向其他域上的资源发送HTTP请求,以确定是否安全发送实际请求。由于跨站点请求可能对用户数据产生影响,因此会像这样进行预检。特别是,如果:

  1. 它使用除GET、 HEAD 或 POST 之外的方法。此外,如果使用POST发送请求数据,并且Content-Type不是application/x-www-form-urlencoded、multipart/form-data或text/plain,例如,如果POST请求使用application/xml或text/xml向服务器发送XML有效负载,则该请求将进行预检。
  2. 它在请求中设置自定义标头(例如,请求使用诸如X-PINGOTHER之类的标头)

2
你的示例试图通过ajax从另一个域加载资源: 跨域请求

我认为我创建了一个更好的示例。这是代码:
<img src="smiley.png" alt="smiley" />
<div id="respText"></div>

<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script>
  $(window).load(function(){        
    $.get("smiley.png", function(){
      $("#respText").text("ajax request succeeded");
    });
  });
</script>

你可以在这里测试该页面。

根据Firebug和Chrome网络面板的显示,图像以状态码200返回,并从缓存中获取ajax请求中的图像:

Firefox: Firebug

Chrome: chrome network panel

因此,我无法找到任何意外行为。


你正在使用<img标签,显然它的图像引用以JS可用的形式存在。跨域不是问题的原因 - 问题是为什么在必须使用background-image时它会有如此奇怪的行为? - NoBugs
这是第二个例子,我使用带有背景图像的div元素。我得到了与我的答案中相同的结果。 - PSanetra
一个“anfrage”是一个请求。 - PSanetra

1

在Ajax请求中,缓存控制一直是一个模糊且有缺陷的主题(示例)。跨域引用问题更加严重。

您提供的fiddle链接来自jsfiddle.net,这是fiddle.jshell.net的别名。每个代码都运行在fiddle.jshell.net域内,但您的代码正在从别名引用图像,浏览器将认为它是跨域访问。

要解决此问题,您可以将两个URL都更改为http://fiddle.jshell.net/img/logo-white.png或只使用/img/logo-white.png


CORS不是问题,正如您可以在algorhythm的答案和fiddle中看到的那样,它在Chromium中正确缓存(304),但在Firefox中不是(它会两次发送整个图像)。 - NoBugs
@NoBugs 我的意思是CORS只是为问题添加了一些限制,而这些限制正是你原始问题的根源。Firefox对于http://jsfiddle.net/img/logo-white.png/img/logo-white.png的缓存处理完全不同,尽管响应头完全相同(来自同一台服务器)。然而,不同的服务器(如imgur.com缓存代理)将返回不同类型的缓存头,并需要进行不同的分析,偏离了你的原始问题。 - Sérgio Castelani

1
Mozilla的热心人提供了一些关于这个问题的细节。显然,Firefox认为“匿名”请求可能与普通请求不同,因此会发出第二个请求,并且不认为具有不同标头的缓存值是同一请求。

https://bugzilla.mozilla.org/show_bug.cgi?id=1075297


0
可能有些靠不住,但我认为以下是发生的情况。
根据http://api.jquery.com/jQuery.get/
dataType
  Type: String
  The type of data expected from the server. 
  Default: Intelligent Guess (xml, json, script, or html).

给出了4种可能的返回类型。没有返回image/gif数据类型。因此,浏览器不会为src文档测试其缓存,因为它正在传递不同的mime类型。


但是jQuery.ajax()方法有一个名为contentType的选项。在那里,您可能可以使用image/gif。但我认为它没有预期的结果。 - algorhythm
真实的,然而在示例代码中,它并未被设置,并且"text/html"与"image/gif"不匹配。 - BReal14
查看 jQuery.ajax 帮助文档(其中包含许多不在 .get 或 .post 中的文档),可以更清楚地了解它的作用——它影响 jQuery 对响应的奇怪处理(自动反序列化,如果响应看起来像javascript,则自动运行脚本等),但它不应该影响实际获取方式。http://api.jquery.com/jQuery.ajax/#jQuery-ajax-settings - NoBugs
同意,但这些选项在示例代码中没有被设置。没有调用.overrideMimeType()等...我看不到任何东西表明默认请求会自动将自己转换为实际的内容类型。大多数帮助文档都集中在文本实体上。关于二进制对象的文档非常有限,甚至可以说没有。我也来这里学习,当然愿意听取所有建议,但我仍然认为ajax方法太蠢了,无法意识到它拥有的内容,并且不会调用浏览器来检查它的缓存,因为它不符合正确的标准。 - BReal14

0

我想要JS可访问的图片

你尝试过使用jQuery使用CSS吗?这非常有趣 - 你可以完全控制CSS元素的CRUD(创建、读取、更新、删除)。例如,在服务器端对图像进行调整大小:

$('#container').css('background', 'url(somepage.php?src=image_source.jpg'
    + '&w=' + $("#container").width() 
    + '&h=' + $("#container").height() + '&zc=1');

令人惊讶的是,即使 JavaScript 等待整个页面加载,图像请求仍会发出新请求!这是 Firefox 和 Chrome 中已知的 bug,还是 jQuery ajax 做了一些不好的事情?

很明显,这不是浏览器的 bug。

计算机是确定性的,并且会按照您告诉它要做的事情来执行(而不是您希望它做的)。如果要缓存图像,则在服务器端完成。根据谁处理缓存,可以处理为:

  1. 服务器(如 IIS 或 Apache)缓存 - 通常缓存经常重复使用的内容(例如:5 秒内 2 次)
  2. 服务器端应用程序缓存 - 通常重用服务器自定义缓存或创建 精灵图像
  3. 浏览器缓存 - 服务器端向图像添加缓存标头,浏览器维护缓存

如果还不清楚,那我想澄清一下:您不应该使用 JavaScript 缓存图像。

理想情况下,我希望ajax根本不需要发出第二个请求,因为它正在请求完全相同的url。
你尝试做的是预加载图片。

Once an image has been loaded in any way into the browser, it will be in the browser cache and will load much faster the next time it is used whether that use is in the current page or in any other page as long as the image is used before it expires from the browser cache.

So, to precache images, all you have to do is load them into the browser. If you want to precache a bunch of images, it's probably best to do it with javascript as it generally won't hold up the page load when done from javascript. You can do that like this:

function preloadImages(array) {
    if (!preloadImages.list) {
        preloadImages.list = [];
    }
    for (var i = 0; i < array.length; i++) {
        var img = new Image();
        img.onload = function() {
            var index = preloadImages.list.indexOf(this);
            if (index !== -1) {
                // remove this one from the array once it's loaded
                // for memory consumption reasons
                preloadImages.splice(index, 1);
            }
        }
        preloadImages.list.push(img);
        img.src = array[i];
    }
}

preloadImages(["url1.jpg", "url2.jpg", "url3.jpg"]);

Then, once they've been preloaded like this via javascript, the browser will have them in its cache and you can just refer to the normal URLs in other places (in your web pages) and the browser will fetch that URL from its cache rather than over the network.

源代码:如何在Javascript中缓存图像

在这种情况下,为什么CSS和AJAX通常具有不同的缓存,好像浏览器对CSS和AJAX请求使用不同的缓存存储?

enter image description here

即使缺乏信息,也不要草率下结论!

One big reason to use image preloading is if you want to use an image for the background-image of an element on a mouseOver or :hover event. If you only apply that background-image in the CSS for the :hover state, that image will not load until the first :hover event and thus there will be a short annoying delay between the mouse going over that area and the image actually showing up.

Technique #1 Load the image on the element's regular state, only shift it away with background position. Then move the background

position to display it on hover.

#grass { background: url(images/grass.png) no-repeat -9999px -9999px; }
#grass:hover { background-position: bottom left; }

Technique #2 If the element in question already has a background-image applied and you need to change that image, the above

won't work. Typically you would go for a sprite here (a combined background image) and just shift the background position. But if that isn't possible, try this. Apply the background image to another page element that is already in use, but doesn't have a background image.

#random-unsuspecting-element { 
    background: url(images/grass.png) no-repeat -9999px -9999px; }
#grass:hover { background: url(images/grass.png) no-repeat; }

The idea create new page elements to use for this preloading technique may pop into your head, like #preload-001, #preload-002, but that's rather against the spirit of web standards. Hence the using of page elements that already exist on your page.


是的,我知道你可以用JavaScript来控制CSS,但在某些情况下这会显得太奇怪和不专业。必须等待jQuery加载完成,然后再设置它。假设您需要立即显示图像,并且需要以编程方式访问该图像以进行画布/宽度/高度等使用,预加载无法帮助。浏览器应该在加载时加载所有图像,因此它应该能够通过ajax请求而无需下载整个文件(在Chrome和旧版IE中似乎可以正确执行)。 - NoBugs

0

服务器决定可以缓存多长时间的内容。但这又取决于浏览器是否遵循。大多数Web浏览器如Chrome,Firefox,Safari,Opera和IE都会遵循它。

我想在这里强调的是,您的Web服务器可能配置为不允许浏览器缓存内容。因此,当您通过CSS和JS请求图像时,浏览器遵循服务器的命令而不会将其缓存,因此它会请求两次图像...


但是使用 jQuery.ajax({url: '.../img/logo-white.png', cache: true}) 将会被缓存。 - algorhythm
1
@algorhythm 是的,但是 CSS 请求在 jQuery 之前,因此您正在缓存第二个请求的第二个响应... - Anshu Dwibhashi
是的,但那不是你的重点。你试图解释,服务器可能配置为防止缓存。而我的代码行可以反驳这一点,不是吗? - algorhythm
2
即使服务器配置了防止缓存,是否遵守这一点完全取决于浏览器。 - Anshu Dwibhashi
是的,很有道理...谢谢。 - algorhythm
1
@algorhythm,AnshumanDwibhashi:cache:true基本上意味着“什么也不做”,并将遵循正常的缓存行为(即从第一个请求中获取资源);只有cache:false会明确防止缓存(jQuery通过在URL后附加随机字符来实现此目的)。 - Bergi

-2

浏览器将在页面上进行2个请求,因为从CSS调用的图像也使用GET请求(而不是ajax),然后才呈现整个页面。

window.load类似于属性,并在页面的其余部分之前加载,然后,在页面加载期间,Ajax中的图像将首先被请求,然后是div中的图像。

如果您想在整个页面加载完成后加载图像,则应改用document.ready()。


1
不是,DOM准备就绪先被调用,然后才是window.onload。后者在加载所有资源,包括图片之后才被调用。 - algorhythm
@algorhythm 更正 - load应该在加载所有资源后调用,正如您在fiddle中所看到的那样,它没有该url,至少无法通过ajax访问而不重新下载整个图像。 - NoBugs
这是我写的:'...后者在加载所有资源之后被调用...'。 - algorhythm

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接