- 背景は、要素の寸法に現れない 測っても見つからない理由
- body の背景は、どこまで敷かれるか キャンバスへの伝播
- ::before に逃がす — fixed と absolute 包含ブロックが変わる
- z-index:-1 が背面に回るための条件 重ね合わせ文脈
- will-change: transform は、どこに書くか 親を昇格させないと効かなかった
- 10手の記録 推測が5回外れた経緯
- まとめの早見表
背景は、要素の寸法に現れない
横方向のはみ出しを探すとき、ふつうは要素の位置と幅を測ります。
document.querySelectorAll('body *').forEach(el => { const r = el.getBoundingClientRect(); if (r.right > innerWidth) /* 犯人候補 */; });
この方法には限界があります。背景は要素の寸法に影響しません。
background-image がどれだけ広い範囲に敷かれても、
getBoundingClientRect() の値は変わりません。
transform)などです。
background-image: none を一時的に入れて症状が変わるか見る。
理屈で追うより確実で、しかも数秒で済みます。
body の背景は、どこまで敷かれるか
body に背景を指定すると、それはビューポート(キャンバス)へ伝播します。
html に背景が無い場合の既定の挙動です。
色ならこれで問題ありません。ところが繰り返しパターンだと話が変わります。
body {
background-image:
linear-gradient(rgba(0,229,255,.025) 1px, transparent 1px),
linear-gradient(90deg, rgba(0,229,255,.025) 1px, transparent 1px);
background-size: 44px 44px;
}
| 敷かれる範囲 | 幅 | 高さ | タイル数 |
|---|---|---|---|
| 文書全体(body 直接) | 360 | 6400 | 約1,300枚 |
| ビューポート1画面 | 360 | 800 | 約170枚 |
縦に長い記事では、タイルが数千枚単位になります。 そしてモバイルでは、スクロール中にアドレスバーの表示が切り替わり ビューポートの寸法が変化します。 そのたびに、この広い範囲の塗りが再計算されることになります。
::before に逃がす — fixed と absolute
背景を body から擬似要素へ移すと、敷かれる範囲を指定できます。
ただし position の選び方で結果が変わります。
| 包含ブロック | 範囲 | 注意点 | |
|---|---|---|---|
fixed | ビューポート | 1画面ぶん | スクロールバー幅を含む |
absolute | 直近の位置指定祖先 | その要素ぶん | 親に position が要る |
fixed の場合
body::before {
content: "";
position: fixed;
inset: 0; /* 常に1画面ぶん */
z-index: -1;
pointer-events: none;
background-image: ...;
}
塗る範囲が最小になります。背景は固定され、内容だけがスクロールします。 薄い模様なら体感差はほぼありません。
absolute の場合
body {
position: relative; /* 基準にする */
overflow-x: clip; /* はみ出しを切る */
}
body::before {
position: absolute;
inset: 0; /* body の寸法に追従 */
z-index: -1;
}
基準が body になるのでスクロールバーを含みません。
一方で範囲は文書全体に戻るため、
overflow-x:clip を併せないと §2 の状態に逆戻りします。
実際、clip 無しで試したときは元の症状が再現しました。
fixed、iPad Safari では absolute + clip
がそれぞれ有効でした。両方を満たす単一の答えは見つけられていません。
最終的には次の §5 の指定を加えることで、どちらの環境でも安定しました。
z-index:-1 が背面に回るための条件
擬似要素を内容の背後に置くため z-index:-1 を使いますが、
これだけでは足りない場合があります。
z-index が負の要素は、その要素が属する重ね合わせ文脈の中で背面に置かれます。
文脈の基準となる要素の背景より前、内容より後ろです。
body が重ね合わせ文脈を作っていないと、擬似要素は
さらに外側の文脈(ルート)へ回ります。
すると body の背景色に隠れることがあります。
body {
position: relative;
z-index: 0; /* これで重ね合わせ文脈になる */
}
z-index:0 は一見すると意味がなさそうですが、
position と組み合わさると重ね合わせ文脈を生成します。
これで z-index:-1 の行き先が body の中に確定します。
opacity が1未満、transform、filter、
will-change(対象プロパティによる)など。
意図せず文脈が生まれて重なり順が変わることも起きるので、
「なぜか前面に出てこない」ときは祖先の指定を疑ってください。
will-change: transform は、どこに書くか
will-change は「この要素はこれから変化する」とブラウザに伝える指定です。
transform を指定すると、多くの場合合成レイヤーへ昇格します。
昇格した要素は、描画がGPU側で完結し スクロール中の再計算から切り離されます。
擬似要素に書いても効かなかった
body::before {
will-change: transform; /* 変化なし */
}
body {
will-change: transform; /* これで安定した */
}
body の描画そのものだったため、
昇格させるべきは親のほうでした。
「背景の問題だから背景を直す」という発想では届きませんでした。
代償
will-change は常時レイヤーを確保します。
body 全体が対象なので、メモリ消費は増えます。
本来は「変化の直前に付けて、終わったら外す」使い方が推奨される指定です。
常時付けっぱなしにするのは、他に手段が無いときの選択と考えたほうがよいと思います。
背景は、どこに敷かれているか
4つの方式で、塗られる範囲がどう変わるかを比べます。 緑の格子が背景、灰色の枠が文書全体です。
fixed のときだけ、塗る範囲が1画面ぶんに収まります。
その代わり幅がスクロールバーを含む——このトレードオフが、
単一の正解に辿り着けなかった理由でした。
10手の記録
この症状を追うのに、10回の試行が必要でした。 推測で提示した仮説は5つ、当たったのは1つです。 同じ道を辿る人のために、外れた分も含めて残します。
まとめの早見表
| 状況 | 対処 |
|---|---|
| 測ってもはみ出し要素が出ない | 背景・影・transform を疑う |
| 背景が原因か確かめたい | background-image: none を一時的に |
| 塗る範囲を1画面に限る | ::before + position: fixed |
| スクロールバー幅を含めたくない | position: absolute + 親に overflow-x: clip |
z-index:-1 が背景に隠れる | 親に position + z-index: 0 |
| スクロール中だけ描画が乱れる | will-change: transform を親に |
| 修正の効果が判定できない | キャッシュを無効にして比べる |
この記事のまとめ
- 背景は要素の寸法に現れない。測って0件でも、原因が無いとは限らない。
bodyの背景はキャンバスへ伝播し、文書全体を覆う。繰り返し模様では枚数が跳ね上がる。fixedはビューポート基準=1画面ぶん。ただしスクロールバー幅を含む。absoluteは親の箱基準。バーは含まないが、範囲は文書全体に戻る。z-index:-1を効かせるには、親が重ね合わせ文脈を作っている必要がある。will-changeは昇格させたい要素そのものに書く。子に書いても親は再計算される。- 常時
will-changeはメモリを消費し続ける。他に手段が無いときの選択。 - 理屈で3回外し、実測で2回当てた。消して試すのが、いちばん速い。
- 縮まないと、あふれる
min-width:0 と overflow の関係 - クリップとスクロールの境界
hidden / clip / overscroll-behavior - 背景と合成レイヤー(この記事)
::before / fixed / will-change