【學習圖片】11.描述性語法

前端小智發表於2023-03-08
本文首發於微信公眾號:大遷世界, 我的微信:qq449245884,我會第一時間和你分享前端行業趨勢,學習途徑等等。
更多開源作品請看 GitHub https://github.com/qq449245884/xiaozhi ,包含一線大廠面試完整考點、資料以及我的系列文章。

使用srcsetsizes來向瀏覽器提供有關影像來源和它們如何被使用的資訊。

在這個模組中,我們將學習如何為瀏覽器提供一系列影像選擇,以便它可以做出最佳的顯示決策。srcset不是在特定斷點切換影像源的方法,也不是為了將一張影像換成另一張。這些語法允許瀏覽器獨立地解決一個非常困難的問題:無縫地請求和渲染一個適合使用者瀏覽上下文的影像源,包括視口大小、顯示密度、使用者偏好、頻寬和一些其他因素。

這是一個巨大的要求 - 當我們只是簡單地標記一個影像的時候,它肯定超出了我們的想象範圍,並且做得好需要比我們能夠訪問的資訊更多。

使用 x 描述密度

一個固定寬度的<img>在任何瀏覽上下文中佔據的視口空間相同,無論使用者顯示器的密度(螢幕上的物理畫素數量)如何。例如,固有寬度為400px的影像在原始的Google Pixel和較新的Pixel 6 Pro上幾乎佔據整個瀏覽器視口 - 這兩個裝置都有一個標準化的412px邏輯畫素寬的視口。

然而,Pixel 6 Pro具有更清晰的顯示:6 Pro的物理解析度為1440×3120畫素,而Pixel為1080×1920畫素,即構成螢幕本身的硬體畫素數量。

裝置的邏輯畫素和物理畫素之間的比率是該顯示的裝置畫素比(DPR)。 DPR是透過將視口的CSS畫素除以裝置的實際螢幕解析度來計算的。

image.png

因此,原始Pixel的DPR為2.6,而Pixel 6 Pro的DPR為3.5

Phone 4是第一個DPR大於1的裝置,報告的裝置畫素比為2--螢幕的物理解析度是邏輯解析度的兩倍。在iPhone 4之前的任何裝置的DPR為1:一個邏輯畫素對一個物理畫素。

如果你在DPR為2的顯示器上檢視該400畫素寬的影像,則每個邏輯畫素被呈現在顯示器的四個物理畫素上:兩個水平和兩個垂直。影像不會從高密度顯示中受益 - 它在DPR為1的顯示器上看起來與在DPR為2的顯示器上看起來相同。

當然,瀏覽器渲染引擎繪製的任何內容 - 如文字、CSS形狀或SVG - 都將被繪製以適應高密度顯示器。但是,從影像格式和壓縮中學到的知識,光柵影像是固定的畫素網格。儘管可能不總是非常明顯,但針對高密度顯示放大的光柵影像會與周圍頁面相比看起來低解析度。

為了防止這種放大,正在渲染的影像必須具有至少800個畫素的固有寬度。當縮小以適應400個邏輯畫素寬的佈局空間時,該800畫素影像源具有雙倍的畫素密度 - 在具有DPR為2的顯示器上,它看起來很清晰。

image.png

地址:https://codepen.io/web-dot-dev/pen/QWBGVyo

由於DPR為1的螢幕無法利用影像的增加密度,因此影像將被縮小以匹配螢幕。如你所知,縮小的影像看起來也很好。在低密度顯示器上,適用於高密度顯示器的影像看起來就像任何其他低密度影像。

在《影像和效能》中所學到的,使用縮小到400px的影像源的低密度顯示器使用者只需要一個固有寬度為400px的源。雖然更大的影像對所有使用者來說都可視,但在小型低密度螢幕上渲染的巨大高解析度影像源將看起來像任何其他小型低密度影像,但速度要慢得多。

具有DPR為1的移動裝置非常罕見,儘管在“桌面”瀏覽環境中仍然很常見。根據Matt Hobbs共享的資料,約18%的GOV.UK瀏覽會話從2022年11月開始報告DPR為1。雖然高密度影像看起來可能符合這些使用者的期望,但它們將產生更高的頻寬和處理成本,特別是對於仍然可能擁有低密度顯示器的舊裝置和較弱裝置的使用者來說,這是特別令人關注的。

使用srcset可確保只有具有高解析度顯示器的裝置接收足夠大的影像源以顯示清晰,而不會將相同的頻寬成本傳遞給具有低解析度顯示器的使用者。

srcset屬性標識一個或多個逗號分隔的渲染影像的候選項。每個候選項由兩個部分組成:一個URL,就像在src中使用的那樣,以及描述該影像源的語法。 srcset中的每個候選項都是由其固有寬度(“w語法”)或預期密度(“x語法”)描述的。

x語法”是“此源適用於具有此密度的顯示器”的簡寫,“2x”後跟的候選項適用於DPR為2的顯示器。

<img src="low-density.jpg" srcset="double-density.jpg 2x" alt="...">

支援srcset的瀏覽器將渲染兩個備選項:high-density.jpg,其中2x適用於DPR為2的顯示器,以及src屬性中的low-density.jpg,如果在srcset中找不到更合適的備選項,則選擇該備選項。對於不支援srcset的瀏覽器,將忽略該屬性及其內容,通常會請求src的內容。

很容易將srcset屬性中指定的值誤解為指令。 2x告知瀏覽器相關原始檔適用於DPR為2的顯示器-有關源本身的資訊。它不告訴瀏覽器如何使用該源,只是告知瀏覽器該源可以如何使用。這是一個微妙但重要的區別:這是一個雙倍密度影像,而不是用於雙倍密度顯示器的影像。

指定“此源適用於2倍顯示器”和指定“在2倍顯示器上使用此源”之間的語法差異很小,但顯示密度只是瀏覽器用於決定要渲染的備選項的眾多相互關聯因素之一,其中只有一些你能夠知道。

例如:單獨地,我們可以確定使用者透過prefers-reduced-data媒體查詢啟用了節省頻寬的瀏覽器偏好設定,並使用它來始終選擇低密度影像,而不考慮其顯示密度-但除非每個開發人員在每個網站上都一致地實施它,否則對使用者來說沒有多大用處。他們可能會在一個網站上尊重他們的偏好,並在下一個網站上遇到一個破壞頻寬的影像牆。

srcset / sizes使用的故意模糊的資源選擇演算法為瀏覽器留出了空間,以決定選擇低密度影像以實現頻寬下降,或基於最小化資料使用的偏好而選擇。我們不需要對如何、何時以及在什麼閾值下承擔責任。承擔瀏覽器更適合為我們處理的責任和額外工作是沒有意義的。

用w來描述寬度

srcset 可以接受第二種型別的描述符,用於影像源候選項。這是一種更加強大的描述符,而且對於我們的目的來說,更容易理解。與標記候選項具有適當尺寸以適應給定顯示密度不同,w 語法描述每個候選源的固有寬度。同樣,每個候選項都是相同的,除了它們的尺寸 - 相同的內容,相同的裁剪和相同的縱橫比。但在這種情況下,你希望使用者的瀏覽器在兩個候選項之間進行選擇:具有固有寬度為 600pxsmall.jpg,和具有固有寬度為 1200pxlarge.jpg

srcset="small.jpg 600w, large.jpg 1200w"

這並沒有告訴瀏覽器如何處理這些資訊 - 只是提供了一個顯示影像的候選項列表。在瀏覽器可以決定渲染哪個源之前,你需要提供更多的資訊:一個描述影像在頁面上將如何渲染的說明。為此,請使用 sizes 屬性。

用 sizes 描述使用情況

在傳輸影像方面,瀏覽器表現出極高的效能。對於影像資源的請求將在樣式表或 JavaScript 的請求之前啟動 - 通常甚至在標記語言被完全解析之前就已經開始了。當瀏覽器發起這些請求時,除了標記語言之外,它對頁面本身沒有任何資訊 - 它甚至可能尚未啟動對外部樣式表的請求,更別提應用它們了。在瀏覽器解析你的標記語言並開始發出外部請求的時候,它只有瀏覽器級別的資訊:使用者視口的大小,使用者顯示器的畫素密度,使用者偏好等等。

這並沒有告訴我們有關影像在頁面佈局中應該如何渲染的任何資訊 - 它甚至不能將視口用作 img 大小的上限的代理,因為它可能佔據水平滾動的容器。因此,我們需要使用標記語言提供這些資訊給瀏覽器。對於這些請求,這是我們唯一能夠使用的資訊。

srcset 一樣,sizes 旨在在標記語言解析後儘快提供有關影像的資訊。就像 srcset 表示“這裡是原始檔及其固有大小”,sizes 表示“這裡是佈局中渲染影像的大小”。描述影像的方式是相對於視口的 - 再次強調,視口大小是瀏覽器在發出影像請求時擁有的唯一佈局資訊。

聽起來有點複雜,但在實踐中理解起來更容易:

<img
 sizes="80vw"
 srcset="small.jpg 600w, medium.jpg 1200w, large.jpg 2000w"
 src="fallback.jpg"
 alt="...">

這裡,sizes 的值告訴瀏覽器,我們的佈局中 img 佔用的空間寬度為 80vw - 視口寬度的 80%。記住,這不是一個指令,而是影像在頁面佈局中的大小的描述。它並沒有說“讓這個影像佔據視口的 80%”,而是“一旦頁面渲染完成,這個影像將佔據視口的 80%”。

示例:https://codepen.io/web-dot-dev/pen/PoBWLYP

作為開發人員,我們的工作已經完成了。我們已經準確地描述了 srcset 中候選源列表和 sizes 中影像的寬度,就像在 srcset 中的 x 語法一樣,剩下的就由瀏覽器來處理了。

但是為了充分理解這些資訊是如何使用的,讓我們花點時間來分析使用者瀏覽器在遇到這些標記時做出的決策:

我們告訴瀏覽器,這個影像將佔用可用視口的80%。因此,如果我們在一個寬度為1000畫素的裝置上渲染這個影像,它將佔用800畫素。然後,瀏覽器將把這個值與我們在 srcset 中指定的每個影像源候選項的寬度相除。最小的源具有600畫素的固有大小,因此:600÷800 = .75。我們的中等大小的影像寬度為1200畫素:1200÷800 = 1.5。我們最大的影像寬度為2000畫素:2000÷800 = 2.5

這些計算的結果(.75、1.5和2.5)實際上是專門針對使用者視口大小定製的 DPR 選項。由於瀏覽器還有關於使用者顯示器密度的資訊,因此它做出了一系列決策:

在這個視口大小下,無論使用者的顯示器密度是多少,都會丟棄 small.jpg 候選源——由於計算出的 DPR 小於1,該源會需要進行任何使用者的放大,因此不適用。在一個 DPR 為1的裝置上,medium.jpg 提供了最接近的匹配——該源適用於1.5的 DPR 顯示,因此它比必要的稍大,但請記住,縮小是一個視覺上無縫的過程。在一個 DPR 為2的裝置上,選擇 large.jpg 作為最接近的匹配項。

如果同一影像在600畫素寬的視口上渲染,所有這些數學計算的結果將完全不同:80vw 現在是480px。當我們把我們的源的寬度除以它時,我們得到1.25、2.54.1666666667。在這個視口大小下,小型small.jpg將在1x裝置上選擇,而medium.jpg 將在2x裝置上匹配。

這張圖片在所有瀏覽上下文中看起來都是相同的:我們的所有原始檔除了尺寸之外都完全相同,每一個都會被渲染成使用者的顯示密度所允許的儘可能銳利的影像。然而,與其為了適應最大的視口和最高密度的顯示器向每個使用者提供large.jpg,使用者將始終獲得最小的合適候選項。透過使用描述性語法而不是指令性語法,我們不需要手動設定斷點並考慮未來的視口和DPR,只需向瀏覽器提供資訊並允許其為我們確定答案。

由於我們的 sizes 值是相對於視口而完全獨立於頁面佈局的,它增加了一層複雜性。很少有一張圖片只佔據視口的百分比,沒有固定寬度的邊距、填充或受頁面上其他元素的影響。我們經常需要使用單位的組合來表達影像的寬度;百分比、empx等等。

幸運的是,我們可以在這裡使用calc()——任何具有響應式影像本地支援的瀏覽器也將支援calc(),使我們能夠混合和匹配CSS單位——例如,一個佔據使用者視口的全寬度,減去兩側1em邊距的影像:

<img
    sizes="calc(100vw-2em)"
    srcset="small.jpg 400w, medium.jpg 800w, large.jpg 1600w, x-large.jpg 2400w"
    src="fallback.jpg"
    alt="...">

描述斷點

如果你花了很多時間來處理響應式佈局,你可能已經注意到這些示例中缺少了一些內容:影像在佈局中所佔空間很可能會在佈局的斷點處發生變化。在這種情況下,需要向瀏覽器傳遞更多細節:sizes屬性接受一組用逗號分隔的候選項,用於指定影像渲染尺寸,就像srcset屬性接受一組用逗號分隔的候選項用於指定影像源一樣。這些條件使用了熟悉的媒體查詢語法。這個語法是第一個匹配:一旦媒體條件匹配,瀏覽器停止解析sizes屬性,然後應用指定的值。

假設你有一張圖片,希望在1200畫素以上的視口上佔據視口寬度的80%,左右各有一個em的內邊距,在較小的視口上則佔據視口的全部寬度。

  <img
     sizes="(min-width: 1200px) calc(80vw - 2em), 100vw"
     srcset="small.jpg 600w, medium.jpg 1200w, large.jpg 2000w"
     src="fallback.jpg"
     alt="...">

示例:https://codepen.io/web-dot-dev/pen/RwBoYRx

如果使用者的視口大於1200pxcalc(80vw - 2em)描述了我們佈局中圖片的寬度。如果(min-width: 1200px)條件不匹配,瀏覽器就會轉到下一個值。因為沒有一個特定的媒體條件與這個值相聯絡,所以100vw被作為預設值使用。如果你使用max-width媒體查詢來編寫這個sizes屬性:”

  <img
     sizes="(max-width: 1200px) 100vw, calc(80vw - 2em)"
     srcset="small.jpg 600w, medium.jpg 1200w, large.jpg 2000w"
     src="fallback.jpg"
     alt="...">

示例:https://codepen.io/web-dot-dev/pen/BaPQOzO

簡單來說,(max-width: 1200px)是否匹配?如果不匹配,繼續。下一個值 calc(80vw - 2em)沒有限定條件,因此這是被選中的。

現在,我們已經向瀏覽器提供了關於影像元素的所有這些資訊 - 潛在來源、內在寬度以及打算向使用者渲染影像的方式 - 瀏覽器使用模糊的規則來確定如何處理這些資訊。

在HTML規範中編碼的源選擇演算法在選擇源的方式上是明確模糊的。一旦源、它們的描述符和影像的渲染方式都被解析了,瀏覽器就可以自由地做任何它想做的事情,我們不能確定瀏覽器會選擇哪個源。

一種語法,它說“在高解析度顯示器上使用此源”,可能是可預測的,但它不會解決響應式佈局中影像的核心問題:保留使用者頻寬。螢幕畫素密度只與網際網路連線速度有較弱的相關性,如果有的話。如果你使用頂級膝上型電腦,但透過計量連線、透過你的手機連線或使用不穩定的飛機WiFi連線瀏覽網路,你可能想選擇低解析度的影像源,無論你的顯示器質量如何。

把最終決定留給瀏覽器允許進行比我們透過嚴格的預定語法進行的效能改進更多的效能改進。例如:在大多數瀏覽器中,使用srcsetsizes語法的img永遠不會請求比使用者已經在瀏覽器快取中擁有的源更小的尺寸的源。當瀏覽器可以無縫地縮小它已經擁有的影像源時,為什麼要為一個看起來相同的源發出新請求呢?但是,如果使用者將其視口縮放到需要新影像才能避免縮放的程度,那麼仍將進行該請求,以便一切看起來符合我們的期望。

雖然這種缺乏明確控制權可能聽起來有點可怕,但是因為我們正在使用具有相同內容的原始檔,與瀏覽器的決策無關,我們不太可能向使用者渲染“破碎”的體驗,就像單源src一樣。

使用 sizes 和srcset

資訊量有點多了。srcsetsizes都是密集的語法,用相對較少的字元描述了大量資訊。也就是說,無論好壞如何,這是經過設計的:使這些語法不那麼簡潔,更容易被我們人類解析,可能會使它們更難被瀏覽器解析。字串中新增的複雜性越多,就越有可能出現解析器錯誤或不同瀏覽器之間行為意外不同的情況。然而,這裡有一個好處:對機器來說更容易閱讀的語法對它們來說也更容易編寫。

對於srcset來說,這是一個明確的自動化案例。很少有人會手工製作多個版本的影像以用於生產環境,而是使用類似Gulp這樣的任務執行器、Webpack這樣的捆綁器、第三方CDN(如Cloudinary)或已經內建在您選擇的CMS中的功能來自動化該過程。只要提供足夠的資訊來生成我們的資源,系統就有足夠的資訊將它們寫入可行的srcset屬性中。

對於sizes來說,自動化要困難一些。系統計算影像在渲染布局中的大小的唯一方法是已渲染布局。幸運的是,出現了許多開發人員工具,將手寫sizes屬性的過程抽象化,效率遠遠超過手動編寫。例如,respImageLint是一段程式碼片段,旨在稽核我們的sizes屬性是否準確,並提供改進建議。

Lazysizes 專案透過推遲影像請求直到佈局建立後,允許JavaScript為我們生成sizes值,以效率為代價實現了一些速度。如果你正在使用完全客戶端黨建框架(如React或Vue),則有許多解決方案可用於編寫和/或生成srcsetsizes屬性,我們將在CMS和框架中進一步討論這些解決方案。

程式碼部署後可能存在的BUG沒法實時知道,事後為了解決這些BUG,花了大量的時間進行log 除錯,這邊順便給大家推薦一個好用的BUG監控工具 Fundebug

交流

有夢想,有乾貨,微信搜尋 【大遷世界】 關注這個在凌晨還在刷碗的刷碗智。

本文 GitHub https://github.com/qq449245884/xiaozhi 已收錄,有一線大廠面試完整考點、資料以及我的系列文章。

相關文章