当我们在浏览器右键保存图片时,为什么浏览器还要再请求一次图片
作者:紫云飞
链接:https://www.zhihu.com/question/564987923/answer/2747449587
巧了,这个问题最近我有研究过。我不能确定题主遇到的是哪种情况,所以我分别给出两种猜测:
图片域名和页面域名同站的情况
我需要先说两个背景,第一个背景是说,最近几年流行的两种新时代的图片格式 AVIF 和 WebP,它们俩呢,其实并没有真正意义上成为新时代的图片格式,因为它们几乎都只在输出端使用,比如通过 CDN 的图片格式转换,自动把源站的 JPG/PNG 转成 WebP 或者 AVIF,虽然用户下载到的图片的确用的新格式,但在输入端,制作图片的人、上传图片的人其实还是在和老格式打交道。
这就导致了一个问题,如果一些普通用户,想要从网页上下载一张图片再编辑、或者通过聊天软件发送、或者上传到别的网站,如果刚刚下载到的是 WebP/AVIF 格式,就会遇到有些软件或网站不兼容新格式的情况,比如 PS、微信、微博都有这个问题,而这会让用户很恼火。
感兴趣的可以看下面详细的背景:
再来说说另外一个背景问题,就是在 Chrome 里用页面另存为的方式保存 PDF 文件的一个 issue,有人在 10 年前反馈的,至今还未解决。就是说很多网站提供的 PDF 文件其实是通过 POST 一个表单的形式打开的,这个时候呢,如果用户想要用页面的另存为功能把这个 PDF 保存到本地,那 Chrome 会怎么实现呢?正常想到的就是从硬盘的 HTTP 缓存里拿到这个 PDF 再保存一遍,的确是这样做的,但是呢,偏偏很多网站给这个请求设置了 Cache-Control: no-store,意味着缓存里没有,那可以再请求一次吗?也不行,因为是 POST 请求,POST 请求从语义上意味着可能改服务器数据,比如可能每次查看这个 PDF 是要付费的,所以不行,Chrome 在这种情况下会直接下载失败。这正是人们十年来反馈的问题,希望 Chrome 能直接从内存里恢复出一个 PDF 文件来,或者弹窗提醒用户,说下载这个文件需要再次执行 POST 请求,让用户选择,至少能完成下载。
接下来就是正题了,右键的图片另存为功能,它和 PDF 页面另存为一样,都是会走请求的,也就同样可能会拿到 HTTP 缓存里的图片,所以你的问题里部分描述是错的,浏览器不是一定会重新发请求获取图片。
什么时候会重新发请求呢?一个是缓存里没有这张图的情况,比如因为带了 Cache-Control: no-store、或者缓存空间不足、或者图片巨大,大于单条缓存的上限,但这些情况概率很低。更大的概率是缓存里虽然有这张图,但是因为某些原因不能用,比如因为 Vary 响应头。
Vary 响应头是 HTTP 协议里用来给同一个 URL 保存多份缓存的,CDN 的图片格式转换功能就是根据浏览器发送的 Accept 请求头来返回不同格式的图片。浏览器带了 accept: image/avif,image/webp 的话, CDN 就会实时的转码出新的格式并返回,如果 Accept 只有 image/*,那就证明它不支持新格式,也就直接返回原始的图片格式 JPG/PNG。这种情况通常源站会配上 Vary: Accept 响应头,告诉 CDN 要给同一个 URL 保存两份不同的缓存,否则可能会出现给不支持 WebP 的浏览器也返回 WebP,就会展现失败。
在 Chrome 99 之前,图片的右键另存为发起的网络请求是不带有 Accept 请求头的,但在网页里发起的请求是带了 Accept 的,这就意味着,如果想右键保存的正是图片服务器转换出的 WebP/AVIF 图片,会因为存在有 Vary: Accept,且一次有 Accept 一次没有,导致浏览器没法复用缓存里的图片。
注意,不使用缓存里的图片这个问题倒不大,关键是重新发起的请求不带 Accept,意味着服务器会认为浏览器不支持新格式,从而返回原始的 JPG/PNG 格式,也就是通过右键另存为下载到的图片格式和刚刚在页面里展现的格式不一样了。
有人在去年把这个表现按照 bug 报个了 Chrome,他说在页面里展示的明明是 WebP 图片,右键另存为到电脑上却成了 PNG,这不科学。Chrome 的人听了也觉得有道理,从技术上来说,另存为的图片格式的确不应该发生变化,于是他们给右键另存为图片的情况补上了 Accept 请求头:
![](https://picx.zhimg.com/80/v2-078bc5a78f3de410b9cec65416b6a85f_720w.webp?source=1940ef5c)
这个改动不光会让图片能另存为成 WebP 格式,还会让请求直接走缓存读取,不重新发起新请求。
再来说点额外的知识,在浏览器里除了右键另存为图片,还可以右键复制图片,还可以拖拽图片到资源管理器或者别的程序里。这三种情况, 在 Chrome 里走的都是不同的代码逻辑,且结果可能完全不同。
看过我之前讲剪切板格式的回答的人可能知道,复制图片并不能复制原始图片格式,而是特定的某种格式,在苹果电脑下的话是 TIFF,不过在粘贴时各种程序最终会转成 PNG,所以可以认为右键复制图片复制的格式是 PNG,且是从内存里复制的,显然不需要再发请求下载一次图片。
拖拽呢,你就不一定能猜到什么表现了,我告诉你,拖拽的时候 Chrome 同样会从内存里拿图片,但是是保持原图的格式。
也就是说,在 Chrome 99 之前,对于同一张服务端识别 Accept 返回的 WebP 图片(原图为 JPG)执行三种保存操作,会得到三种不同的格式:
- 右键另存为 -> JPG(发起新请求下载)
- 右键复制 -> PNG(从内存拿,不发请求)
- 拖拽到桌面 -> WebP(从内存拿,不发请求)
在 Chrome 99 及之后只有第一个变了:
- 右键另存为 -> WebP(从磁盘缓存拿)
一开始讲的第一个背景里也提到了,普通用户其实不喜欢下载到 WebP/AVIF 格式,所以 Chrome 的这个改动虽然从技术上看是合理的,但在普通用户的体验上却是退步了,有个叫 Unsplash 的图片网站的技术人员就提了 issue,希望 Chrome 能回滚到老的表现。
![](https://pic1.zhimg.com/80/v2-667d5be2aa102aa2b056c614b417647a_720w.webp?source=1940ef5c)
这个老哥的经历也很惨,他其实是在 Chrome 99 之前报的 issue,他原本报的内容是希望 Chrome 在拖拽图片到桌面的时候可以和右键另存为一样,可以让用户下载到 JPG,这样用户体验更好,但是呢,过了几个月后,不仅拖拽出的格式没有改,右键另存为也变成了 WebP,他提的 issue 也被标记成了 WontFix。
如果题主发现这个问题的时候用的是 Chrome 99 之前的版本(国产浏览器都符合),且图片服务器的确是检测 Accept 才返回了 WebP,且加了 Vary: Accept 响应头,且图片域名和页面域名是没有跨站,那可以试试更新版本的 Chrome,右键另存为图片要重新发起请求的表现已经没了。
但是题主遇到的是这种情况的概率可能不大,因为条件太多,我发现的百度图片几乎满足所有这些条件,除了它没有加 Vary: Accept,因为百度它不是传统需要回源到源站的 CDN 服务,它自己就拥有 CDN,不需要源站通过 Vary: Accept 告诉它也可以实现为两种格式分别缓存。
图片域名和页面域名跨站的情况
和百度图片不一样,其实大部分站点里图片所用的域名和页面自身域名是跨站的,这种情况下,就像题主说的,右键另存为图片,目前的 Chrome 的确是一定会发起新的网络请求,同样是因为无法使用已有的缓存,原因是:
在这篇回答里,我说到:
因为浏览器缓存其实是个哈希表,每个缓存有一个独立的 key,以前 URL 自身就是这个 key,现在这个 key 里包含了页面的域名部分,比如这个资源在知乎的页面里被缓存的话,这个缓存的 key 就类似于 "https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/jquery/3.5.0/jquery.min.js|https://zhihu.com"。
Chrome 从大概 85 开始,给缓存加上了额外的 isolation key,也就是额外加上了所在页面的一级域名。比如说在 http://taobao.com 的页面里加载了很多 http://alicdn.com 的图片,这个时候图片缓存的 key 里都带了 http://taobao.com 前缀,比如类似 taobao.com|https://alicdn.com/foo.jpg 。
这个时候你右键另存为这些图片,由于另存为的请求不属于页面本身,而是浏览器行为,就没有所属的页面,所以它的 isolation key 就成为图片 URL 自己的一级域名,也就是 http://alicdn.com,缓存的 key 就成了 alicdn.com|https://alicdn.com/foo.jpg,缓存表里并没有这个 key,于是重新下载。
能不能让图片另存为时的 isolation key 也设置成当前页面的域名?实现的话应该是不难,但我觉得 Chrome 的人不太会愿意改,因为的确另存为被看成是和页面无关的行为,比如 DevTools 里就看不到这个另存为的请求,而且不光缓存的 key 不一样,另存为发起的请求还有其它一些细节的差异,比如 sec-fetch-site 这个请求头,如果在页面中请求的话值可能是 cross-site,如果是另存为,值是 none。sec-fetch-site 这个请求头,如果在页面中请求的话值可能是 no-cors,如果是另存为,值是 navigate,也就是相当于看成是一种导航请求,也就是看成了在新标签单独打开了这个图片 URL 一样。除非真的能有什么用户体验上的提升,否则单纯就减少一个请求的话,我都不好意思去提这个需求。
有人可以试试火狐里的表现,测试方法是,打开淘宝首页,关掉网络,右键保存图片,如果失败,证明也是受了缓存隔离的影响。
不知道说明白了没有,总结一下就是:图片另存为的操作的确不像图片复制和图片拖拽一样是内存操作,它是走了发请求的方式,意味着可能会命中缓存也可能不命中缓存,不命中的原因一个是 Vary 响应头,一个是因为缓存隔离,那样的话就会发起真正的网络请求。