Chrome 79 - 遲到了 6 年的 rem/em 修正

眼看 Chrome 79 大概再兩周就要 release 啦!

image-20191204101404584

對大多數人來說,這個也就是 Chrome 例行更新的一個版本。

而對我,Chrome 79 修正了一個最近一年多在我心裡佔有一席之地的 bug,我曾經努力試著解決它,最後似乎也找到自己應對它的方法。而現在它要被修掉了,多少有點感觸 😥。

本文特別想趕在 Chrome 79 正式釋出之前寫出來,讓在閱讀的你也可以一同經歷這個變化的過程。

話說從頭,可能會有點長,如果想直接看結論可以點這邊

Chrome 的「最小字體」傳說

我想小有接觸前端的人大概都知道 Chrome 有個 最小字體是 12px 的傳說,然而時過境遷、滄海桑田,2019 的現在還是如此嗎?

實際上現在再去 Google “chrome minimum font size” 得到的結果跟討論大都是 2013 年以前的內容,就算有稍微近期的文章,也大都是參考早期文章寫出來的。

從搜尋結果裡面可以找到這個 issue:

https://bugs.chromium.org/p/chromium/issues/detail?id=36429

裡面提到幾件重要的事情:

  • Chrome 預設的最小字體是和 UI 使用的語言有關的
  • 大部分的 UI 語言預設最小字體是 1px
  • 複雜 UI 語言(多為亞洲語言)的預設最小字體是 10px/12px
  • 2010年時還沒有辦法從設定該改最小字體的設定

而到了 2019 的現在,最小字體已經可以從設定裡面更改了,位置就在「設定」->「自訂字型」

image-20191201151933781

可能很多人會是在看到這篇文章之後才第一次打開這個設定畫面,比對一下上面列出來的項目可以發現繁中 UI 的 Chrome 的最小字體預設值就是 12px。

我猜如果從安裝時就是英文版 UI,預設值應該會是 0px/6px。

曾經是傳說的最小字體 12px,僅限於某些 UI 語言(基本上就是中文、日文、韓文、泰文)的瀏覽器。

現在其實是使用者把它調低就可以解決的問題。當然,對於開發者來說還是一個無法跨越的障礙。

做一個 fiddle 來測試一下「沒調設定」跟「調過設定」的結果: https://jsfiddle.net/pc035860/jgtfdL98/4/

image-20191201155500430

Twitch 聽說你是 rem 排版大師?

自從近年各瀏覽器對 rem 的支援度有提高之後,坊間出現一些使用 rem 為單位進行網頁排版的文章,並且漸漸也有些網站開始使用這種排版方法(雖然我覺得還是相對蠻少的)。

建議往下看之前要先瞭解一下用 rem 單位進行排版大概是怎麼樣進行,可以參考下面這篇 sitepoint 的文章 https://www.sitepoint.com/understanding-and-using-rem-units-in-css/

普遍常見的起手式,是把 rem base 設為 1px 或 10px,這樣一來直接使用 rem 單位換算的過程會簡單很多。

其中又以在 <html> 上用 font-size: 62.5% 來設定為 10px 最常見:

html {
  /* 16 * 0.625 = 10 */
  font-size: 62.5%;
}

Twitch 的大改版衝擊

Twitch 在 2018 年 2 月左右做了一次重大改版,把原本用的 Ember.js 換成 React,這一次改版同時也把網站的排版單位改為 rem,而這也是我們繁中 Chrome 使用者的噩夢開端(好像講得有點誇張了)。

改版之後,原本頻道的資訊頁面變得非常奇怪。下面示範截圖是目前最新版,當然也還是用 rem 單位排版。

正常來說應該是這樣:

image-20191201163414370

改版後就直接跑掉:

image-20191201163641701

這還只是第一眼就覺得不對勁的部分。

用 Firefox 打開 Twitch 來比較就可以看出整個網站都有點不一樣,Chrome 整體似乎大了一號,這邊就單取側邊欄來對比:

diff

WTF!? 不是, Twitch 難道沒有測過嗎…

怎麼一回事

來看一下 Twitch 的 <html> 怎麼寫的

image-20191201170216996

也就是他們的 rem base 是設定為 10px,接著看看對應的 side-nav class 寬度是多少呢?

image-20191201170846555

10 * 24 = 240,他們希望的寬度是 240px,那麼實際上在我們繁中 Chrome 顯示會是多寬呢?

image-20191201171409173

288 / 24 = 12,結果不言而喻啦!

Chrome 的最小字體被套用在 <html> 上,導致 rem 的 base 直接用 12px 起跳。

類似問題,其實也曾經在 Apple 的網站上出現過,不過當時是用 em 為單位。

眼尖的你可能會在上面的截圖中發現,字體大小是相同的。那麼我們就接著來看字體大小的部分:

image-20191203221847471

14 / 1.4 = 10 剛好是設定的 rem base 10px,也就是說 font-size 部分並沒有受到 <html> 上 12px 的影響,依然故我的使用了 10px 來計算。接著我順便檢查了 padding, margin 之類的屬性,發現結果跟上面 width 一樣是用 12px 來計算的,看來只有使用在 font-size 時候的計算是分開的。

於是我們稍微修改一下上面論述,得到一個小結論:

Chrome 的最小字體被套用在 <html> 上,導致 rem 單位使用在 font-size 以外的屬性上時,其 base 直接用 12px 來做計算

這邊我做了一個簡單的驗證用 fiddle: https://jsfiddle.net/pc035860/athfj0dc/18/

裡面的 .box ,希望透過 rem 設定出的正確數值如下:

寬: 100px
高: 100px
padding: 10px
字體大小: 14px

後面也還會用到這些數值來做正確性的驗證,稍微有個印象就好了。

好啦,都是 Chrome 的錯

前面說過現在最小字體可以調整是吧,那我們把最小字體的設定調到 10px 以下不就好了嗎?

那我們就拿上面驗證用的 fiddle 來比較一下,「保持預設 12px」跟「調整到 0px」的結果:

01

怎…怎麼這樣,竟然沒有變!?

UI 語言的陷阱

冷靜想想,Twitch 團隊在改版過了一年多的現在都沒有修正是什麼原因?

  • 無法 reproduce 問題
  • 只有少部分的人有問題,不用管他們

難道問題出在語言上? 讓我們把 Chrome UI 語言設定成英文試試看。

image-20191201180109879

藍框處顯示目前是中文,點擊紅框處可以切換為英文。(註: Mac 使用者需要從「系統偏好設定」->「地區與語言」裡面改)

接著再用上面的 fiddle 來測一次最小字體 0px:

image-20191203225916663

沒錯,把 UI 切成英文後就一切正常了。

順帶一提,初始安裝英文 UI Chrome 的最小字體應該是 0px/6px 其中一個,可以直接得到上圖的結果。因此如果系統語言不是上述亞洲語系的話,壓根就沒有機會看到這個 bug。

總之雖然原因不明,但看起來在繁中 UI 的 Chrome 下,就算已經調整過最小字體,在 <html> 上用 font-size: 62.5% 並沒有辦法把字體壓到 12px 以下。

繁中 UI 的最後一搏

看到這裡是不是你也覺得繁中 UI 的 Chrome 使用者真是太絕望了…

在我多番嘗試之後發現,剩下最後一手可以解決這個問題:

<html> 上指定 rem base 的方式直接寫成 font-size: 10px

雖然會失去瀏覽器設定「字型大小」的彈性,但不失為一個解決辦法。

用這個更新的 fiddle 來測試: https://jsfiddle.net/pc035860/athfj0dc/20/

02

其實避免碰到上面這種 rem 單位排版問題的方法就是總是將 base 設為 12px 以上。雖然會造成使用時換算單位的麻煩,但總比碰到這種難以 debug 的問題來的好。

同場加映一個也碰到類似問題的網站 issue: https://github.com/squidfunk/mkdocs-material/issues/911

擴充功能: Twitch REM fix

於是我為了我往後美好的瀏覽 Twitch 體驗,我決定做將這個 workaround 做成 Chrome extension。

起初還有「要判斷當前 UI 語言之後再來套用」之類的天真(?)想法,靜下心來仔細想想,沒有問題的人根本不會來裝這個extension

釋懷後的我,緩緩地開了個新的 CSS 檔案,在裡面輸入:

/* file: contentscript.css */

html {
  font-size: 10px !important;
}

大功告成。

截圖跟寫說明花的時間還比打這段 CSS 多…

基本上這個 extension 會在 *.twitch.tv 下面無腦套用 contentscript.css 的內容,有修正版面需求的人可以到下面的 Github 網址逛逛:

https://github.com/pc035860/twitch-rem-fix

所以 Chrome 79 是?

好了,上面說了這麼多,總算可以進入正題啦!

來看這個 2013 年的 Chrome issue

https://bugs.chromium.org/p/chromium/issues/detail?id=308862

這個 issue 是我在最小字體的相關搜尋中翻到的,主訴是「rem/em 用作單位時,在計算結果前就被套用了最小字體的限制」,怎麼聽起來這麼耳熟?

Use computed font-size for em/rem resolution.

This makes us match how Firefox applies minimum font-size and makes us not break relative length computation when pages use a font-size less than the user’s minimum font-size as a basis for widths, heights, margins, padding, etc.

從中推敲一下,未修正前的版本大概可以這樣表示(純示意,我沒有去翻 source code):

const minFontSize = 12  // 瀏覽器設定「最小字體」

// 純計算後得到的 <html> font-size
const rootFontSizeComputed = 16 * 0.625  // 10
// 調整過後的 <html> font-size
const rootFontSizeAdjusted = Math.max(minFontSize, rootFontSizeComputed)

const targetNumber = 24  // 目標數字
const targetRem = 2.4  // 手算 rem 數字: 24 / 10 = 2.4

// 用於 font-size 之外的屬性: width, height, padding, margin
const resultRelativeLength = rootFontSizeAdjusted * targetRem
// Math.max(12, 10) * 2.4 = 28.8

// 用於 font-size 屬性
const resultFontSize = Math.max(minFontSize, rootFontSizeComputed * targetRem)
// Math.max(12, 10 * 2.4) = 24

這個 issue 最近終於修正完畢,在 Chrome 79 會 release。

在修正後,resultRelativeLength 的式子大致上變成這樣(純示意,我沒有去翻 source code):

// adjusted -> computed
const resultRelativeLength = rootFontSizeComputed * targetRem
// 10 * 2.4 = 24

我實際從 beta channel 安裝 Chrome 79 的測試的結果也沒有問題,真的修正啦!

什麼都不用做,只要更新到 Chrome 79 以上、上面的 extension 也不用裝,各種 rem 單位排版造成的問題就迎刃而解。

如果你常在看的網站 (例如: Twitch),突然變了(回原)樣,千萬別太驚訝喔 😉

值得特別提出的是,在 Chrome 79 的繁中 UI 上用 font-size: 62.5% 還是沒有辦法把渲染字體壓到 12px 以下,就算你調了最小字體的設定也一樣。 更多討論請見本文最後的附錄。

TL;DR

  • Chrome 的最小字體其實現在可以設定了,並且根據安裝時的 UI 語言,預設值會不同

    • 中文、韓文、日文、泰文等 是 10px/12px
    • 其他通常是 6px 或以下
  • Twitch 改版後使用了 rem 單位排版,在繁中 UI 的 Chrome 看起來會跑掉
  • 在繁中 UI 的 Chrome 底下,如果 font-size 用百分比單位表示,就算調了最小字體設定也沒有辦法被壓到小於 12px

    • 因此造成 Twitch 改版後版面跑掉
    • 把 UI 語言調成英文可以解決這個問題
    • font-size: 10px 取代 font-size: 62.5% 是一種 workaround
    • 我把 workaround 做成 extension 了
  • Chrome 79 會修正套用最小字體的方式

    • rem 單位排版計算錯誤的問題會消失
    • 可能有些你看習慣的網站會有變化 (例如: Twitch)
    • 套用在 <html> 上時,繁中 UI 百分比 font-size 的最小值依然是 12px (見附錄)

後記

從去年改版後, Twitch 版面的問題就一直困擾著我,有點像是揮不走的蒼蠅,每次打開網站就看到 😨

直到最近(2019/9)又再一次改版,新的版面搭上崩壞的 rem 單位真的是慘不忍睹,我只好弄了一個 extension 先撐著。

偶然發現下一版 Chrome release (大概兩周後)就可以躺著解決(?),發這篇文正是為了紀念這期間我內心的掙扎,還有製作擴充功能(跟它的說明)的流下的汗水。

我會懷念你的, Chrome 78 (含以前)!

感謝你讀到這邊,下次有有趣的東西要分享的話,我會再回來的。

附錄: 最終負載測試

以下的測試用 Chrome 79 beta 進行,「最小字體」都已經設定為 0 。

這次在旁邊加了使用同樣 rem 做為長寬的 div 來對照: https://jsfiddle.net/pc035860/jgtfdL98/35/

分別對 繁中 / 英文 UI 版本 以及 font-size: 10px / font-size: 62.5% 的組合做交叉測試,得到的結果如下:

appendix

  • rem 數值用在 width, height 上不受 font-size 指定方式的影響
  • 在使用者可以設定的「最小字體」之外,還有別的隱藏字體大小門檻

    • 只在 font-size 用百分比設定時套用
    • 根據 UI 語言會有不同的數值

      • 繁中 UI 是 12px
      • 英文 UI 是 9px
    • 從 rem base 的角度來看,門檻應該是套用在計算完 rem -> px 結果之後,否則理應會影響我們用 rem 表示的寬跟高

比較一下 Firefox Developer Edition 71 的測試結果:

appendix-ff

我想 Chrome 應該是出於好意啦… 嗯…

最後把這個隱藏門檻也考慮進去,整理進前面提到的式子:

/**
 * Chrome 79 繁中版
 *
 * 使用 font-size: 62.5% 來設定 root 字體大小
 **/

// 瀏覽器 UI 最小字體隱藏門檻,只在使用百分比時啟用
// 繁中 UI: 12
// 英文 UI: 9
const minPercentageFontSize = 12

// 瀏覽器設定「最小字體」
const minFontSize = 0

// 是不是用百分比設定的: 是
const isRootFontSizePercentage = true

// font-size 屬性專用的調整函數
const adjustFontSize = value => {
  // step1: 套用瀏覽器設定的「最小字體」
	const step1 = Math.max(minFontSize, value)
  
	if (!isRootFontSizePercentage) {
    // 如果不是用百分比設定,不需要到 step2
    return step1
  }
  
  // step2: 用百分比設定,font-size 套用隱藏門檻
  const step2 = Math.max(minPercentageFontSize, step1)
  return step2
}

// 純計算後得到的 <html> font-size
const rootFontSizeComputed = 16 * 0.625  // 10

// 調整過後的 <html> font-size
const rootFontSizeAdjusted = adjustFontSize(rootFontSizeComputed)
// 12

const targetNumber = 10  // 目標數字
const targetRem = 1  // 手算 rem 數字: 10 / 10 = 1

// 用於 font-size 之外的屬性: width, height, padding, margin
const resultRelativeLength = rootFontSizeComputed * targetRem
// 10 * 1 = 10

// 用於 font-size 屬性,套用調整函數
const resultFontSize = adjustFontSize(rootFontSizeComputed * targetRem)
// 12