1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

dvh 使用時にアドレスバーの格納・展開で画面がカクつく問題

Last updated at Posted at 2025-05-15

dvh とは?

dvh(dynamic viewport height)は、デバイスのビューポートの高さに基づいて要素の高さを指定できるCSSの単位です。

css
.section {
  height: 100dvh;
}

スマートフォンなど、利用者によって画面サイズが異なる環境でも、それに合わせて自動で高さを調整してくれるため、柔軟なレイアウト設計が可能になります。

便利な反面、意図しない挙動も

スクロールで起こる“かくつき現象”

スマートフォンのモバイルブラウザでは、スクロール操作に応じてアドレスバーが自動的に格納されたり、再展開されたりします。
これによりビューポートの高さが変わり、100dvh で指定した要素の高さも再計算されます。

その結果、画面全体がガクッと動くように見えてしまうのです。

問題の現象をご覧ください。

左がカクつくページ、右は修正したものです。

Timeline-1.gif

再現ページを作りましたので、スマートフォンでアクセスしてみてください。

レイアウト構成

今回の現象は以下のような要件で起こりました。

再現要件の具体例

  • ファーストビューに固定ヘッダーがある
  • ヘッダー分のスペースを確保するため、下の要素に margin-top を設定している
  • ビジュアル画像の高さを calc(100dvh - ヘッダーの高さ) で調整している

これにより、100dvhで指定した要素が再計算されてしまい、ページ全体がガクッと動いてしまい、ユーザビリティを大きく損なう原因となってしまいます。

コード

実際のコードを見てみましょう。

修正前(カクつく方)は 48px(mt-12) のマージンを上部に持ち、ヘッダー分下げています。
それだけだと画像がファストビューから下にはみ出てしまうので、 48px100dvh から引くことで、ぴったりデバイスの高さに収まるということになります。

page.tsx
<main>
  <div className="h-[calc(100dvh-48px)] mt-12">
     <Image src={photo} alt="" priority className="h-full w-full object-cover" />
  </div>
</main>

しかし、アドレスバーが格納・展開するたびに calc の計算が走り、画像を画面内に収めようと伸びたり縮んだりすることでカクつきが発生してしまいます。

結論: svh を使う

コメントをいただきました。 svh を使えと……

svh はスモールビューポート(アドレスバー以外の画面部分)しか見ないので、アドレスバーがあろうがなかろうが高さは変わらない。よって、カクつきは起きないのでした。

ここから下は読まなくても大丈夫です。

そのまま dvh を使って処理する場合

どうしても dvh を使わなければいけないとき

……があるかはわかりませんが、一応やり方として残しておきます。

初期状態を保存して再利用する

ページに初めてアクセスした段階で、画像の高さは計算されていることに着目します。

この状態を useEffect で初回レンダリング時のみ計算し、 useState で管理することで2回目以降の計算が行われずカクつきを防止することができます。

page.tsx
// カスタムフックを作成してロジックを分離
function useInitialViewHeight(): string {
  // 初期値も計算済みの値を使用してちらつきを防止(初期レンダリングで使用)
  const [viewHeight, setViewHeight] = useState<string>("calc(100dvh - 48px)");

  useEffect(() => {
    // 初回レンダリング時にのみ高さを計算
    // CSS変数の100dvhはモバイルでスクロール時に変動することがあるため
    // window.innerHeightで固定値をピクセル単位で取得して再計算を防止
    const height = window.innerHeight;
    setViewHeight(`${height}px`);
    // 空の依存配列で初回レンダリング時のみ実行
  }, []);
  return viewHeight;
}

export default function AfterPage() {
  const viewHeight = useInitialViewHeight();
  return (
    <main>
      <div className="mt-12" style={{ height: `calc(${viewHeight} - 48px)` }} >
        <Image src={photo} alt="" priority className="h-full w-full object-cover" />
      </div>
    </main>
  );
}

useEffect 内で同じ計算を行っているのに、初期値にも calc(100dvh - 48px) を指定する必要があるのかと疑問に思う方もいるかもしれません。

useEffect が発火する前に、あらかじめ calc(100dvh - 48px) を初期値として指定しておくことで、初期レンダリング時のちらつきを防止でき、結果としてユーザビリティの向上につながります。

1
0
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?