0.5px は描けるかもしれないし、描けないかもしれない(feat. DPR)

profile image

なぜ 0.5px のボーダーはデバイスによって見えたり見えなかったりするのか。DPR(Device Pixel Ratio)を理解すれば理由が分かる。

この記事は Jetbrains's Coding Agent Junie junie logoによって翻訳されました。誤訳があれば教えてください!

フロントエンド開発をしていると、デザインに 0.5px のボーダーが指定されていることがよくあります。しかし border-width: 0.5px と書いても、開発者ツールではたいてい 1px と表示されます。「ブラウザは 0.5px を描けないの?」と思いきや、環境によっては正確に 0.5px が表示されることもあります。

この差を生む鍵が Device Pixel Ratio、すなわち DPR です。

DPR(Device Pixel Ratio)とは?

DPR は、1 つの論理ピクセル(CSS ピクセル)を表現するために使われる物理ピクセルの数を示します。言い換えると:

画面上で 1 論理ピクセルを表示するのに必要な物理ピクセルの倍率です。

text
DPR = 物理ピクセル数 ÷ 論理ピクセル数

物理ピクセル(Physical Pixel)

物理ピクセルはディスプレイを構成する実際のハードウェアのピクセルで、発光する最小単位です。

たとえば iPhone 16 Pro の解像度が 1206 x 2622 なら、横方向に 1206、縦方向に 2622 の物理ピクセルが存在するという意味です。

論理ピクセル(Logical/CSS Pixel)

論理ピクセルは CSS で用いる抽象的な単位です。コードで width: 100px と書くときの px が論理ピクセルを指します。

論理ピクセルは、デバイスごとの物理解像度に依存しない一貫した見た目のサイズを提供するための概念です。これにより開発者は、物理解像度を意識せずとも同じ CSS でさまざまなデバイスに対応できます。

先ほどの例を続けると、iPhone 16 Pro の論理解像度は 402 × 874 です。物理ピクセル(1206 x 2622)を論理ピクセル(402 × 874)で割ると 3。つまりこのデバイスの DPR は 3 です。

DPR によるピクセル対応

DPR を理解したところで、各 DPR で論理ピクセルがどのように物理ピクセルに割り当てられるかを見てみます。

  • DPR 1(一般的なモニター):CSS 1px = 物理 1px
    • 1 論理ピクセルを 1 物理ピクセルで描画
  • DPR 2(MacBook の Retina ディスプレイ):CSS 1px = 物理 2x2px(合計 4)
    • 1 論理ピクセルを 4 物理ピクセルで描画
  • DPR 3(iPhone Pro など):CSS 1px = 物理 3x3px(合計 9)
    • 1 論理ピクセルを 9 物理ピクセルで描画

dpr-example.png

では、0.5px はいつ描けるの?

DPR 1(一般的なモニター)

一般的なモニターでは、1 論理ピクセルは 1 物理ピクセルで表されます。では 0.5px は? 物理的に「半分のピクセル」は存在しません。ピクセルは最小単位だからです。

そこでブラウザはこれを検知して、丸め込みやアンチエイリアス(ぼかし)処理を行います。そのため、意図した極細線ではなく、1px の太さになったり、ぼやけた線に見えたりします。

text
0.5px(論理) × DPR 1 = 0.5px(物理・不可能)→ 1px に丸められる

DPR 2(MacBook Retina)

DPR が 2 のデバイスでは状況が変わります。1 論理ピクセルを 2×2 の物理ピクセルで表現できます。

text
0.5px(論理) × DPR 2 = 1px(物理・可能!)

0.5 論理ピクセルに DPR 2 を掛けると、ちょうど 1 物理ピクセルになります。つまり 1 つの完全な物理ピクセルで表せるため、DPR 2 以上のデバイスでは 0.5px が正しくレンダリングされます。

ブラウザのズームと DPR

興味深いことに、ブラウザの拡大・縮小を変更すると window.devicePixelRatio も変化します(ブラウザによって挙動は異なる場合があります)。

javascript
// DPR 1 のデバイスにて
// 100% ズーム
console.log(window.devicePixelRatio); // 1
// 200% ズーム
console.log(window.devicePixelRatio); // 2
// 50% ズーム
console.log(window.devicePixelRatio); // 0.5

Chromium 系ブラウザがズームで DPR を変更する挙動については、過去に議論があります:

Update window.devicePixelRatio on browser zoom (content upscaling)

ブラウザズームの仕組み

ブラウザズームは CSS ピクセル自体の物理的な大きさを変えます。200% にすると、各 CSS ピクセルが占める物理領域が 2 倍になります。

重要なのは、要素の CSS ピクセル値そのものは変わらないという点です。width: 128px と定義された要素は、200% ズームでも 128 CSS ピクセルのままです。変わるのは各 CSS ピクセルが占有する物理的な面積です。

  • 100%:CSS 1px = 物理 1px(基準)
  • 200%:CSS 1px = 物理 2x2px(各 CSS ピクセルの面積が 4 倍)
  • 50%:CSS 1px = 物理 0.5x0.5px(各 CSS ピクセルの面積が 1/4)

0.5px はズームでどう変わる?

text
100%(DPR 1):0.5px × 1 = 0.5px(物理)→ 1px に丸められる ✗
200%(DPR 2):0.5px × 2 = 1px(物理)→ 描画可能 ✓

つまり 200% に拡大すると、有効 DPR が 2 となり 0.5px の描画が可能になります。

逆に、Retina ディスプレイ(DPR 2)で 50% に縮小すると、有効 DPR は 1 に下がります。この場合、0.5 論理ピクセルは再び 0.5 物理ピクセルとなり(表現不可能)、一般的なモニターと同様にシャープな 0.5px 表現ができなくなります。

DPR と画像レンダリング

DPR はボーダーのような UI 要素だけでなく、画像のレンダリングにも大きく関わります。

高精細ディスプレイと画像の品質

DPR 2 の Retina ディスプレイで 300x300px の画像を表示すると仮定します。ブラウザはこの 300 CSS ピクセルを表現するために、物理的には 600x600px の領域を使用します。

では、画像ファイル自体が 300x300px しかなかったら? ブラウザは 600x600 の物理領域に合わせて拡大せねばならず、結果として画像はぼやけます。

text
CSS 300px × DPR 2 = 物理 600px
しかし画像ファイルは 300px → 拡大が必要 → ぼやける

これを避けるには、DPR に合ったサイズの画像を用意するべきです。

  • DPR 1:300x300px で十分
  • DPR 2:600x600px(2×)を提供
  • DPR 3:900x900px(3×)を提供

srcset とブラウザの自動選択

ユーザーのデバイスは DPR がまちまちです。常に最大サイズの画像を配信すると、低精細ディスプレイでは無駄に大きいファイルをダウンロードしてしまいます。逆に小さな画像だけだと、高精細ディスプレイではぼやけて見えます。

HTML の srcset はこれを解決します。

html
<img
  srcset="
    photo-320w.jpg,
    photo-480w.jpg 1.5x,
    photo-640w.jpg 2x
  "
  src="photo-640w.jpg"
  alt="説明"
  style="width: 320px"
/>

上記のコードでは:

  • 画像は CSS により 320px で表示される
  • srcset に複数解像度の画像を定義する

ブラウザはデバイスの DPR を確認し、最適な画像を自動で選択します。

  • DPR 1 デバイスphoto-320w.jpg をロード(1x は省略可)
  • DPR 1.5 デバイスphoto-480w.jpg をロード
  • DPR 2 以上のデバイスphoto-640w.jpg をロード

高精細ディスプレイ(DPR ≥ 2)では、より大きな画像ファイルが読み込まれ、くっきりとした画質になります。

Next.js の Image コンポーネントの動作

Next.js の <Image> コンポーネントは、この原理を自動で処理してくれます。

typescript
<Image
  src="/photo.jpg"
  width={300}
  height={300}
  alt="説明"
/>

Next.js は内部的に複数サイズの画像を生成し、srcset も自動で作成します。開発者が複数の画像を用意したり、srcset を手書きしたりする手間を減らしてくれます。

まとめ

最初は「なぜ 0.5px が描けないのだろう?」という素朴な疑問から始まりましたが、DPR を理解すると 0.5px が描けない理由だけでなく、同じ CSS がデバイスによって描画結果が異なる理由、高精細ディスプレイで画像がかえってぼやけて見える理由、srcset の存在意義など、さまざまなことが腑に落ちます。


参考