包包baobaolin.com
date
entry
055
topic
tooling
rev
—

iframe 放到畫面外,loading="lazy" 的圖 5 秒內一張都沒載;換成 --dump-dom 又一張不漏

把頁面載進 left: -10000px 的 iframe 檢查圖片,六張的 naturalWidth 全是 0,伺服器一次請求都沒收到。同一頁換成 --dump-dom,六張全載。

檢查行動版排版時,我習慣用同源 iframe 把頁面載進來,再從父頁讀 scrollWidth(截圖的寬度不可靠,entry 027 講過)。在 tigerfasteners.com 的前身 screwkingtw 這樣量的時候,我順手讀了每張圖的 naturalWidth,一整批都是 0,看起來像圖檔全壞了。

圖其實沒壞。那些 <img> 帶著 loading="lazy",我的 iframe 又放在畫面外。今天照同樣的擺法重做,沒載的圖連請求都沒有發出;我推測當時也是同一個原因:瀏覽器判定它們離視窗太遠。這篇要談的是 lazy loading 在 iframe 裡怎麼判斷「快進入視窗了」,依序講四件事:哪些擺法會載、哪些不會;距離為什麼是拿最上層視窗算;同一頁換成 headless 的 --dump-dom 反過來一張不漏,以及我追到哪裡;量之前要怎麼把它關掉。

實測:iframe 擺在哪裡,決定載幾張

當時的量測腳本沒有留下來,以下數字都是今天重做的。環境是 Google Chrome 156.0.8078.48、--headless=new、macOS,--window-size=1200,800(寬度大於 500,避開 entry 027 那個下限)。本機用 python3 -m http.server 供檔,Node.js 22.19 寫的 CDP(Chrome DevTools Protocol,從外部控制瀏覽器的協定)腳本開頁,5 秒後讀結果。

iframe 寬 375。裡面的頁面在 top 0、600、1500、2500、4000、8000px 的位置各放一張 200×100 的 lazy 圖,每張網址帶不同的 query string,伺服器記錄看得出哪一張被請求過。父頁在 iframe 的 load 事件後等 3 秒,把每張圖的 naturalWidth 寫進頁面,CDP 腳本讀的是這份紀錄:

iframe 擺法載入的圖(top)
左上角,375×8000、600、1500
同上,加 visibility: hidden0、600、1500
左上角,高度拉成 90000、600、1500
top: 3000px(畫面下方)無
left: -10000px(畫面外)無

伺服器記錄和 naturalWidth 對得上,沒載的圖連一次請求都沒有。有兩點和我原本的直覺不同。iframe 拉高到 9000px、讓整頁在裡面完全攤開,載入的張數沒有變多;visibility: hidden 也沒有讓它少載。在這幾組條件裡,改變結果的是 iframe 在最上層視窗裡的位置;高度從 800 拉到 9000、加上 visibility: hidden,都沒有改變載入的張數。

為什麼:距離是拿最上層的視窗算

iframe 有自己的 viewport(可視區域),但 lazy loading 的交集根不是它。HTML 規格的 lazy loading 那節替每個 Document 建一個 IntersectionObserver(交集觀察器),選項只給了 scrollMargin,沒有指定 root。IntersectionObserver 規格寫明,root 為 null 時,交集的根是「頂層瀏覽脈絡的 document」,叫做 implicit root;巢狀瀏覽脈絡(也就是 iframe)裡的元素,也是拿來和它比。計算時仍會經過 iframe 自己的裁切與 scrollMargin,但最後比的對象是最上層視窗。

換句話說,iframe 裡的圖要不要載,是看它在最上層視窗裡離可視範圍多遠。iframe 推到 left: -10000px,裡面每張圖都在最上層視窗左邊一萬像素外,連 top 0 那張也一樣,所以在我觀察的 5 秒內一張都沒載。

至於多遠算「快到了」,web.dev 的〈Browser-level image lazy loading for the web〉(2024-08-13 更新)寫 Chrome 在快速連線(4G)的距離門檻是 1250px,3G 以下是 2500px,數值寫死,API 改不了。我在最上層頁面直接放圖量:top 1960 的載了,1980 的沒載。同一次執行裡讀到的 innerHeight 是 713(開頁 5 秒後讀的,和 entry 027 少掉 87px 的是同一個高度)。1960 − 713 = 1247,1980 − 713 = 1267,所以這次的轉折點夾在 1247 到 1267px 之間,和 1250 相容,但兩個點量不出精確值。這是我從數字推回去的,沒有去查 Chrome 把本機連線判成哪一種。上面表格那組比較粗的結果(1500 載、2500 不載)也和它相容。

最上層頁面本身不可見時,結果也一樣。用 CDP 的 Target.createTarget 加 background: true 開分頁,document.visibilityState 是 hidden,連 top 0 的圖都沒載。

反方向:--dump-dom 一張不漏

上面的測試都是 --headless=new 加 CDP 開頁。同一個 Chrome 換成命令列的 --dump-dom 流程,結果就反過來了。最上層頁面放那六張圖,用 --dump-dom 跑(加不加 --virtual-time-budget 都一樣),或加上 --virtual-time-budget 用 --screenshot,六張全部被請求。另做一頁在 load 事件裡記下狀態,用 --dump-dom 跑(screenshot 那組只有伺服器記錄):六張的 naturalWidth 都是 200,loading 屬性讀回來仍是 "lazy",visibilityState 是 visible,innerHeight 是 713。iframe 擺在 left: -10000px,用 --dump-dom --virtual-time-budget=8000 跑,也是六張全載。

原因我沒有追到。HTML 規格規定腳本被停用時不延後載入,但這裡我的腳本有跑,上面那些狀態就是它寫進頁面的。我讀了同版本的 components/headless/command_handler/headless_command.js:它用 Target.createTarget 開頁、Page.navigate、等 load 事件,沒有看到和 lazy loading 有關的設定;同目錄的 headless_command_handler.cc 也搜不到 lazy 這個字。

所以同一份頁面,兩種跑法給出相反的答案。用 CDP 開的 headless 分頁會漏掉畫面外的圖,--dump-dom 一張不漏,兩邊都不代表使用者滑到那裡時會看到什麼。

量之前先改成 eager

既然量測方法會左右結果,就要在量之前把 lazy 關掉。在父頁等 iframe 的 load 事件,把裡面每張圖的 loading 改成 eager:

f.addEventListener("load", () => {
  for (const im of f.contentDocument.images) im.loading = "eager";
  // 我的測試是固定等 3 秒再讀 naturalWidth
});

放在 left: -10000px 的 iframe,這樣改完六張全載。HTML 規格寫,loading 屬性改成 Eager 狀態時,會執行那張圖的 lazy load resumption steps,讓它繼續載入,所以只改屬性就夠了。

我當初的筆記寫的是「改成 eager,再重設 src」。這次重測,重設 src 是多餘的:伺服器記錄裡六張圖各被請求兩次,一次是改 eager 觸發的原網址,一次是重設後的新網址。我重設時在網址後面加了 &eager,網址不同,不會走快取;用同一個網址重設會怎樣,我沒有測。

我的分法:要檢查的是「圖檔在不在」,就不看 naturalWidth,直接把每個 src 拿去發 HTTP 請求看狀態碼,這和頁面怎麼排、lazy 怎麼判斷都無關。要量的是版面,例如圖片撐開後的高度或 scrollWidth,就先把 iframe 裡的圖全部改成 eager、等它們載完再量。

如果只記得一件事

naturalWidth 是 0 的時候,先查那張圖有沒有被請求過。當初那批「破圖」沒有留下伺服器記錄;今天重做的這組,沒載的圖在記錄裡一筆都沒有,瀏覽器從頭到尾沒去拿。iframe 放在哪裡、用哪種模式開瀏覽器,本身就是量測的一部分;這兩個前提沒寫下來,量出來的數字就說不清楚是頁面的問題還是工具的問題。

修訂紀錄

  1. 首次發布