本文通過兩個(gè)圖片預(yù)加載案例引起的緩存相關(guān)問題,探討了圖片預(yù)加載處理技術(shù),和瀏覽器網(wǎng)絡(luò)請求以及緩存機(jī)制的一些問題。
2017-04-25 By Herbert Chow
問題起源分析:
一、騰訊游戲:征服星際 戰(zhàn)出未來 移動(dòng)視頻h5中(如圖1),采用了預(yù)加載圖片技術(shù),先有一個(gè)loading頁,然后一個(gè)視頻,之后是一個(gè)落地頁結(jié)束。
在預(yù)加載圖片時(shí),圖片會(huì)有一定的概率發(fā)生重復(fù)請求(重復(fù)加載)的情況并且被重復(fù)加載的圖片會(huì)出現(xiàn)閃爍,就號像根本就沒有預(yù)加載一樣,原因不明,如(圖2),而且如果通過自己手寫函數(shù)進(jìn)行預(yù)加載圖片,會(huì)有極大幾率發(fā)生重復(fù)請求的想象;而該用pxloader.js進(jìn)行圖片加載的話,幾率大大減少,但不為100%不出現(xiàn)。

圖1

圖2
二、陰陽師扭蛋預(yù)約h5(如圖3):與上問題一相似,使用網(wǎng)易內(nèi)部組件trueload組件進(jìn)行加載,在預(yù)加載圖片時(shí),會(huì)有一定幾率會(huì)重復(fù)加載zhaohuanzhen_的序列幀套圖,本來只有46張,后面重復(fù)加載到了85張,或者92張(完全重復(fù)請求了一遍)。流程是,先loading頁,再到登陸頁,再到抽獎(jiǎng)頁面,最后是結(jié)果頁面。在抽獎(jiǎng)頁面,會(huì)用到canvas調(diào)用loading時(shí)緩存的序列幀圖片。

圖3
注:當(dāng)圖片重復(fù)請求時(shí),會(huì)造成圖片閃爍,有可能是因?yàn)樾蛄袔瑒?dòng)畫切換比較連貫,所以看起來閃爍會(huì)比較明顯,而平時(shí)hover時(shí)看可能不那么明顯。本質(zhì)問題應(yīng)該還是圖片重復(fù)請求的問題。
問題假設(shè):
一、是否只要在頁面中請求一次圖片A并且成功后,那么圖片A在后續(xù)的設(shè)置img.src或者設(shè)置bgi時(shí),都不會(huì)發(fā)生重復(fù)請求,而是直接用緩存;
二、是否只要圖片的路徑是一樣的,讀取了一次成功后,就不需再次請求,直接讀緩存。
論證猜測(不供證明,僅供參考):
一、用Pxloader或者trueLoad等組件進(jìn)行加載時(shí),會(huì)比自己手寫加載函數(shù)(大神請忽略)會(huì)好很多,實(shí)驗(yàn)證明,自己手寫預(yù)加載函數(shù)一般會(huì)缺少一些邏輯判斷,導(dǎo)致效果不好,會(huì)有較大概率出現(xiàn)重復(fù)請求的情況。查閱一下Pxloader的源碼,發(fā)覺,簡單的一個(gè)圖片加載函數(shù),代碼量也還是挺大的,不是我們自己一百幾十行就能搞定的。
舉例:有可能出現(xiàn)bug原因是,我們自己寫的加載函數(shù)還沒加載完圖片,然后業(yè)務(wù)邏輯代碼已經(jīng)又進(jìn)行了一次圖片的請求,這樣就會(huì)造成同一張圖同時(shí)被請求了兩次。
成因剖析(第一部分):預(yù)備概念的理解
1、圖片src或者background-image (下面簡稱bgi)設(shè)置和切換問題:
參考鏈接中提到,“創(chuàng)建的Image對象會(huì)加載一次,實(shí)際DOM樹中的元素設(shè)置 src 或 background-image 時(shí)又會(huì)加載一次”,只要image對象被創(chuàng)建,并且設(shè)置其src屬性時(shí),瀏覽器就會(huì)發(fā)起請求,或者h(yuǎn)tml中dom結(jié)構(gòu)中的圖片img標(biāo)簽src發(fā)生變化,big發(fā)生變化時(shí),也會(huì)發(fā)生請求。
2、瀏覽器對于圖片請求的流程
參考鏈接:http://er.dadaaierer.com/?p=431
參考鏈接中提到:
關(guān)于圖片緩存
如果圖片已經(jīng)在緩存中:
1)瀏覽器不會(huì)發(fā)起請求去請求圖片,瀏覽器直接從緩存中拿圖片。
2)而設(shè)置過期時(shí)間會(huì)強(qiáng)迫瀏覽器在訪問頁面的時(shí)候去請求圖片,如果圖片已經(jīng)在緩存中,并且正在被重新請求,瀏覽器會(huì)把最后修改時(shí)間加入在HTTP頭中,這就是傳統(tǒng)的GET請求,如果圖片沒有被修改,服務(wù)器會(huì)返回一個(gè)304代碼,所以對于瀏覽器的請求服務(wù)器會(huì)返回下面的兩種代碼:
200–瀏覽器沒有緩存。
304–瀏覽器已經(jīng)緩存了圖片,但是需要驗(yàn)證最后修改時(shí)間
也就是說,瀏覽器在需要進(jìn)行圖片操作之前,會(huì)先查看本地是否有緩存,如果有,會(huì)先讀取緩存;如果沒有,才會(huì)去發(fā)起網(wǎng)絡(luò)請求。
成因剖析(第二部分):分析原因
一、如圖4,是騰訊外星人h5在一定概率刷新時(shí),發(fā)生重復(fù)請求,此刻注意到網(wǎng)絡(luò)部分,size圖片請求時(shí)間是0ms,而圖片大小其實(shí)是沒有標(biāo)明的,寫著 from menory cache 和 from disk cache,此刻還注意到,網(wǎng)絡(luò)請求時(shí)200而不是應(yīng)該是語氣中的304,十分奇怪。

圖4
二、如圖5,陰陽師扭蛋h5,有一定概率,在發(fā)起200請求之后預(yù)加載圖片后,發(fā)起重復(fù)請求并且返回狀態(tài)碼304。

圖5
三、這里有幾點(diǎn)要注意
1、200和304跟緩存的聯(lián)系
2、請求發(fā)起者initiator
3、size(from memory cache和from disk cache)
四、知識點(diǎn)再次分析
對于上面三點(diǎn)
1、參考鏈接: https://www.oschina.net/question/1395553_175941
參考文中提到,
其實(shí), 200 OK (from cache) 是瀏覽器沒有跟服務(wù)器確認(rèn),直接用了瀏覽器緩存;而 304 Not Modified 是瀏覽器和服務(wù)器多確認(rèn)了一次緩存有效性,再用的緩存。200(from cache) 是速度最快的,因?yàn)椴恍枰L問遠(yuǎn)程服務(wù)器,直接使用本地緩存.304 的過程是, 先請求服務(wù)器, 然后服務(wù)器告訴我們這個(gè)資源沒變, 瀏覽器再使用本地緩存.
結(jié)論: 需要設(shè)置 200 from cache. 這樣才是解決問題之道.
所以說,其實(shí)200(from cache)并沒有重新請求下載圖片,而是和304是相似的原理,就是瀏覽器并沒有重新下載圖片,只是用了本地緩存,只不過瀏覽器會(huì)多了一重請求驗(yàn)證。至于這個(gè)具體是返回200from cache還是304,貌似需要看服務(wù)器返回的東西是否有設(shè)置ETag值了(這部分需要詳細(xì)了解的話,請自行查閱資料)。
2、initiator是請求發(fā)起者,說白了就是你在哪里執(zhí)行了設(shè)置img.src屬性或者設(shè)置了bgi。在扭蛋項(xiàng)目中,筆者進(jìn)行了預(yù)加載(由trueload.js發(fā)起)完成之后,馬上執(zhí)行了一段由index.js執(zhí)行的設(shè)置new Image()數(shù)組保存圖片這么一個(gè)操作,用于后面的序列幀動(dòng)畫。而其實(shí)initiator:other標(biāo)明的就是一些html的資源文件,包括了js,html,css之類,所以圖5這里的other,就是index.js。相同的,在騰訊外星人h5中的首次請求和重復(fù)請求的發(fā)起者分別就是pxload.js和index.js(indexjs中執(zhí)行到了設(shè)置bgi的代碼)。
3、核心關(guān)鍵
size標(biāo)明,304的時(shí)候,瀏覽器會(huì)向服務(wù)器發(fā)起請求,這個(gè)請求容量非常小,相對于200完整請求的20k完整圖片而且,只有20B左右的大小,實(shí)際上,它返回的是304的結(jié)果本身,而不是圖片數(shù)據(jù)。
重中之重的是這個(gè)from menory cache(內(nèi)存緩存) 和 from disk cache(磁盤緩存)
參考鏈接:http://blog.csdn.net/myloveyaqiong/article/details/52762795
(圖6)文中提及,安卓系統(tǒng)對于網(wǎng)絡(luò)圖片緩存機(jī)制是,優(yōu)先讀取Memorycache,找不到之后會(huì)讀取Diskcache,最后都沒有才會(huì)發(fā)起網(wǎng)絡(luò)請求。
翻閱《webkit技術(shù)內(nèi)幕》,書中提及,瀏覽器也是如此,當(dāng)m cache和d cache中都沒找到資源時(shí),會(huì)發(fā)起網(wǎng)絡(luò)請求獲取最新資源。

圖6
成因剖析(第三部分):結(jié)論總結(jié)
一、回顧問題假設(shè)一,答案是否定的。原因是進(jìn)了頁面以后,即使用了預(yù)加載技術(shù),在后面的邏輯中再次創(chuàng)建并設(shè)置新的img.src或者bgi時(shí),依然要經(jīng)過上面提及的圖片資源獲取步驟:
優(yōu)先讀取Memorycache,找不到之后會(huì)讀取Diskcache,最后都沒有才會(huì)發(fā)起網(wǎng)絡(luò)請求。也就是說,舉例,一開始用了trueload預(yù)加載了圖片a0.jpg之后(此時(shí)是trueload發(fā)起),圖片被加入到了內(nèi)存緩存和磁盤緩存中,而一段時(shí)間(或者一段邏輯跑完之后,來到index.js的某段代碼),在index.js里面再次設(shè)置新的img.src時(shí),由于某種原因,index.js發(fā)起獲取圖片a0.jpg在memorycache內(nèi)存緩存里面找不到了,于是只能去diskcache磁盤緩存里面找,這個(gè)時(shí)候就會(huì)引起一個(gè)重復(fù)請求的現(xiàn)象。
二、回顧問題假設(shè)二:答案也是否定的。根據(jù)上述獲取資源原理,同一路徑資源的圖片,即使在預(yù)加載或頁面中有了第一次加載之后,后續(xù)代碼段中再次訪問該路徑圖片,依然有可能會(huì)造成前者能成功訪問memorycache里的資源,而后者則失敗從而導(dǎo)致要到diskcache里面去找圖,這樣的情況。
三、筆者尚不熟悉m cache的d cache處理數(shù)據(jù)的具體原理,尚未弄清為何導(dǎo)致memorycache內(nèi)圖片資源丟失,或者訪問不了的情況,導(dǎo)致別的邏輯代碼在二次訪問的時(shí)候需要去d cache里面找資源,這可能和時(shí)間,系統(tǒng)內(nèi)存,或者initiator請求發(fā)起者有關(guān)。還請各路大神指教一下。
四、至于返回結(jié)果是200from cache還是304,則需要服務(wù)器那邊設(shè)置。但是可以明確一點(diǎn)的是,如果圖片在訪問頁面時(shí)被請求成果過一次之后,那么后續(xù)再次訪問則會(huì)有緩存,無論了是否重新發(fā)出請求,磁盤緩存中已經(jīng)是有緩存到的了,所以在一定程度上已經(jīng)是成功達(dá)到了圖片預(yù)加載的目的。也就是說即使是重復(fù)請求了,那也是返回200from cache還是304,能夠快速使用緩存,不必重新加載200成功完成圖片請求。只不過再進(jìn)一步優(yōu)化的話,就是重復(fù)請求都不用了而已。
解決方案:(暫時(shí)只提供思路,后面會(huì)補(bǔ)上詳細(xì)說明以及demo等)
一、采用專業(yè)的圖片預(yù)加載插件,會(huì)比自己重新寫的預(yù)加載函數(shù)要嚴(yán)謹(jǐn)和高效(高手請忽略),一般移動(dòng)推薦pxloader,pc推薦jq的imgpreload。
二、一般情況下,大家只會(huì)在預(yù)加載時(shí)load一遍圖片,然后后面訪問圖片時(shí)就直接設(shè)置img.src或者bgi:url(地址),這樣在通常情況沒有問題,但是有可能在一定概率下發(fā)生重復(fù)請求。建議改成在load圖片時(shí),把需要load的圖片中的重點(diǎn)圖片,比如序列幀這種需要同一個(gè)階段大量調(diào)用一組圖片,這時(shí)就可以把這組圖片保存在一個(gè)圖片數(shù)組變量中imgs[ ],然后后面就直接使用這個(gè)imgs[ ],不要重新創(chuàng)建對象再賦值圖片地址。
三、調(diào)整預(yù)加載和訪問的代碼邏輯順序。如果不想像方案二那樣浪費(fèi)一個(gè)全局變量,可以再頁面loading頁加載完成后,不要馬上調(diào)用“設(shè)置imgsrc保存到新數(shù)組,以便后續(xù)調(diào)用”的邏輯代碼,而應(yīng)該放到后面業(yè)務(wù)中的,比如點(diǎn)擊按鈕之后才調(diào)用,這樣可以解決剛進(jìn)入頁面時(shí)并發(fā)請求過多導(dǎo)致頁面卡頓的問題。
四、如果是采用bgi較多的項(xiàng)目,在調(diào)用預(yù)加載函數(shù)時(shí),同時(shí)加載css,會(huì)發(fā)生css內(nèi)的bgi地址和預(yù)加載js調(diào)用的地址相撞上,而發(fā)生重復(fù)請求的想象。所以,解決方法是,進(jìn)頁面時(shí)先把該css文件設(shè)置disabled="",屏蔽掉css加載,完了之后再remove掉disabled屬性。
五、根據(jù)垃圾回收機(jī)制,可以把只調(diào)用一次的大量圖片集,用全局圖片數(shù)組變量保存起來,并在完成業(yè)務(wù)調(diào)用后,清除該全局變量imgs=null,回收不必要的內(nèi)存。
其他補(bǔ)充:
待定