动态

详情 返回 返回

亞像素渲染:瀏覽器如何處理小數像素的渲染? - 动态 详情

前言

最近遇到一個這樣的問題,在一些機型上的loading轉圈動畫看起來有點抖,轉起來像個橢圓,心想會不會是這個icon寬高不同造成的,但看了一眼代碼裏面寬高寫的是一樣,按理來説這個loading應該是一個正圓,旋轉起來不應該抖才是的。

比如這樣:

<div class="w-20px h-20px border-rd-50% loading"></div>

寬高相等的一個正圓,旋轉起來看着怪怪的。事實上這是由於rem單位轉換導致出現的小數像素(亞像素)問題

可以看到0.2rem計算過後的值為19.72px,這樣就出現了亞像素,但是它寬高依然還是相等的,旋轉起來也不應該出現抖動的現象🤔

這應該跟瀏覽器的渲染有關係,計算出來的像素為小數,那麼對於小數像素瀏覽器是如何進行渲染的?

CSS值的處理過程

CSS值的定義到最終渲染實際上會經過一系列的步驟,這一過程在W3C Recommendation中有介紹,整個過程一共分為6步:

  • 聲明值:應用於元素的每個屬性都會為它提供一個聲明值,當然也可能存在多個,比如在多個樣式表中重複聲明
  • 級聯值:這一步其實就是在計算樣式屬性的權重,從而得到一個權重最高的值
  • 指定值:它一般等於級聯值或者默認值,繼承屬性用的繼承值 inherit,非繼承屬性將用初始值 initial,也可以顯式的設置 initial/inherit/unset 等關鍵字,從而保證每個元素上的每個屬性都存在一個值
  • 計算值:這一步是為CSS計算得出的值,轉為需要使用的像素值(色彩值等等),注意這裏最終得到的是絕對單位,比如rem在這一步就會轉換成px
  • 使用值:獲取計算值並完成所有剩餘計算的結果,使其成為文檔格式化中使用的絕對理論值。
  • 實際值:使用值原則上可以直接使用,但用户代理可能無法在給定環境中使用該值。例如,用户代理可能只能渲染具有整數像素寬度的邊框,因此可能必須近似於已使用的寬度。此外,元素的字體大小可能需要根據字體的可用性或font-size-adjust屬性的值進行調整。實際值是進行任何此類調整後的已使用值。
屬性 聲明值 級聯值 指定值 計算值 使用值 實際值
font-size font-size: 1.2em 1.2em 1.2em 14.1px 14.1px 14px
width width: 80% 80% 80% 80% 354.2px 354px

亞像素

像素是成像面的基本單位也是最小單位,通常被稱為圖像的物理分辨率。如果成像系統要顯示的對象尺寸小於物理分辨率時,成像系統是無法正常辨識出來的。亞像素是一種抽象概念,用於以邏輯像素的分數表示渲染對象的位置或大小,主要用於佈局和命中測試。當前實現將值表示為 1/64 像素的倍數。這使我們能夠使用整數數學並避免浮點不精確。儘管佈局計算是使用 LayoutUnits 完成的,但繪製時的值仍與整數像素值對齊,以與設備像素對齊。

在使用em, rem這樣的相對單位時, 瀏覽器計算出來的px很可能不是整數,進而在一些顯示設備上出現亞像素渲染問題,比如:圓形變橢圓、圖片顯示不全有切割、元素之間有縫隙等

瀏覽器如何計算亞像素

比如,我們在頁面上寫了一條0.3px的線,那麼瀏覽器的計算值是多少?

.line {
  width: 100px;
  height: 0.3px;
  margin-top: 30px;
  background-color: black;
}

那麼這個值最終是怎麼算出來的呢?文檔上好像沒有特別説明瀏覽器的亞像素計算方式,估計是各瀏覽器的實現都有所不同。

以Google瀏覽器為例來驗證亞像素的計算方式:

比如上圖的0.3px,得到的計算值為0.296875,而Google瀏覽器亞像素表示為1/64 的像素

0.3*64 = 19.2  // 得到了0.3px對應的亞像素

Math.floor(0.3*64) * 64   // 再將得到的亞像素向下取整後再轉為像素,剛好等於0.296875

但是我發現如果都按這種計算方式,有些亞像素算出來的值是有偏差的。

比如0.9px,瀏覽器的計算值為0.898438px

這種就不能以向下取整再轉像素,而是要把小數位取到0.5再轉像素

0.9*64 // 57.6

// 轉為 57.5

57.5 / 64  // 再轉為小數,得到的值為0.8984375

所以結論就是:(Google)

  • 小數位像素先轉為亞像素後得到的值不超過0.5的向下取整後,再轉為像素
  • 小數位像素先轉為亞像素後得到的值超過0.5的將小數位取到0.5,再轉為像素

亞像素與像素對齊方案

對於亞像素與像素的對齊webkit內核會有兩種對齊方案:

上圖中,灰色格子代表物理像素,藍色區域表示亞像素計算值,黑色區域表示最終 亞像素 -> 像素 的對齊結果。

enclosingRect

x: floor(x)
y: floor(y)
maxX: ceil(x + width)
maxY: ceil(y + height)
width: ceil(x + width) - floor(x)
height: ceil(y + height) - floor(y)

這種方式採用的是向上取整的方式來與物理像素對齊,保證能完全覆蓋渲染的物理像素,這個方案只在少部分地方用到,如渲染svg,為了保證盒子能完整包裹矢量圖。

這種方式可能會導致盒模型溢出的風險。

pixelSnappedIntRect

x: round(x)
y: round(y)
maxX: round(x + width)
maxY: round(y + height)
width: round(x + width) - round(x)
height: round(y + height) - round(y)

這種方式則是採用四捨五入的方式來對齊離自己最近的一個物理像素,但整體上來看並不是簡單的四捨五入,而是需要考慮相鄰元素之間的佔位與補充。

這種方式的好處是能夠保證最終渲染的物理大小不超過原來的大小,使得在屏幕等分出現小數的情況也不會溢出到下一行。

瀏覽器是如何渲染亞像素的

上面我們瞭解了瀏覽器是如何計算出亞像素的,但是亞像素只會出現在瀏覽器的計算值中,但瀏覽器繪製時的值仍需要與整數像素值對齊,以保證與設備像素對齊,當與設備像素對齊時,邊緣將與最近的像素對齊,然後相應地調整大小。這可確保底部/右側邊緣和總寬度/高度最多相差一個。

獲取元素寬高的一些方法:

  • getComputedStyle: 返回一個對象,該對象在應用活動樣式表並解析這些值可能包含的任何基本計算後報告元素的所有 CSS 屬性的值。(計算值)
  • offsetWidth/offsetHeight:返回一個元素的佈局寬高,但需要注意的是這個屬性將會 round(四捨五入) 為一個整數。
  • getBoundingClientRect:這個值按理來説應該對應的是使用值,但也不能當成實際值

從開發者角度我們好像並不能直接通過JS去獲取到元素的真實渲染寬度,也就是上面CSS值處理過程中提到的實際值

那怎麼去驗證瀏覽器是怎樣去渲染亞像素的呢?這個時候可以上設計工具了:PS、figma等等都可以...

<div class="outer_box">
    <div ref="innerBox" class="inner_box" v-for="index in 5" :key="index">
        {{ list[index-1]?.computedWidth }} - {{ list[index-1]?.offsetWidth }}
    </div>
</div>
nextTick(() => {
    innerBox.value?.forEach((item: HTMLElement) => {
        list.value.push({
            computedWidth: getComputedStyle(item).width,
            offsetWidth: item.offsetWidth,
        })
    })
})

可以得到這樣一個內容:

把它導入figma中進行測量:

得到第一個矩形的寬度為82px

前兩個總寬度為165px,所以可以得出第二個矩形的寬度為83px

依此類推,我們可以測量出這五個矩形的真實渲染寬度分別為:82px、83px、82px、83px、82px

82+83+82+83+82 = 412 總寬度正好等於屏幕寬度412px。

那麼瀏覽器是按什麼規律來這樣渲染的呢?

  • 第一個矩形原本寬度為82.3984px,按四捨五入取整,實際渲染寬度為82px,但是在邏輯空間上第一個矩形佔據了第83個像素中的0.3984px的位置,所以下一個元素在繪製時應該加上這部分
  • 那麼第二個矩形的寬度就變成了82.3984 + 0.3984 = 82.797 ,按四捨五入取整,實際渲染寬度為83px。但在邏輯空間上這裏應該會空出83 - 82.797 = 0.203px
  • 所以第三個矩形會先填滿上一個空出的0.203px,那麼相當於寬度減少0.203px為82.3984 - 0.203 = 82.195px ,按四捨五入取整,實際渲染寬度為82px,但是在邏輯空間上它又會佔據後一個像素的0.195px的位置,同理下一個元素在繪製時也會加上這一部分
  • 那麼第四個矩形的寬度就變成了82.3984 + 0.195 = 82.593 ,按四捨五入取整,實際渲染寬度為83px,同樣邏輯空間上會空出83 - 82.593 = 0.407px
  • 第五個矩形會先填滿上一個空出的0.407px,相當於寬度為82.3984 - 0.407 = 81.991px,按四捨五入取整,實際渲染寬度為82px

很明顯這裏採用的是pixelSnappedIntRect方案來對齊渲染的。

結論

亞像素引發的典型問題

  1. 圖形失真

    • 正圓變橢圓、直線邊緣模糊、圖標鋸齒化
    • 動畫旋轉時抖動(如Loading圖標呈現"橢圓旋轉"效果)
    • 極細邊框(如0.3px)因舍入歸零導致消失
  2. 佈局崩塌

    • 相鄰元素小數像素累加導致間隙(如82.5px + 82.5px = 165px,但實際渲染可能為82px + 83px = 165px,產生1px錯位)
    • 百分比佈局中微小誤差引發換行/溢出(常見於flex/grid佈局)
  3. 跨端差異

    • 不同瀏覽器亞像素處理策略不同(Chrome 1/64精度 vs Firefox 1/60精度)
    • 高分屏縮放(如150%縮放時,12.5px計算值實際渲染為12px或13px)

亞像素問題本質是數學精度物理像素限制的根本性衝突,瀏覽器試圖用邏輯亞像素(如1/64像素)模擬小數佈局,但最終仍需將計算結果對齊到設備物理像素網格。目前來説這種問題好像並沒有什麼太好的辦法去解決,我們應該主動規避亞像素產生

user avatar toopoo 头像 yinzhixiaxue 头像 tizuqiudehongcha 头像 dengzhanyong 头像 gaoxingdeqincai 头像 stephentian 头像 hulaxingxingxing 头像 jsliang 头像 webshijie 头像 luomg1995 头像 _6085362b65292 头像 dragonir 头像
点赞 26 用户, 点赞了这篇动态!
点赞

Add a new 评论

Some HTML is okay.