9
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?

CSSアニメーションが再発火しない?void element.offsetHeightの正体と使いどころ

Posted at

はじめに

ある日、JavaScriptでスタイルを動的に変更したのに
アニメーションが再発火しない?なぜ?🤔

そんな経験、ありませんか?
この問題、実はブラウザの最適化が裏で効いていて、「同一フレーム内でのスタイル変更」がバッチ処理されてしまうのが原因です。

そこで登場するのが、以下のような一文です。

void element.offsetHeight;

え、voidって何してるの?
offsetHeightの値って使ってないけど、意味あるの?

今回はそんな 謎の一文 と、アニメーション再発火の実用的な使いどころを紹介します。

これは「知ってる人は知ってる」系のテクニック
一見意味不明だけど、フロントエンド現場で困った時の救世主になる「魔法の一行」です!

結論

これはリフローを強制的に発生させるためのコードです。

このコードは次のような意味を持ちます。

  • offsetHeight を 読み取る(=ブラウザにレイアウト確定を促す)
  • その戻り値(number型)は不要なので、voidで捨てる
  • 目的はただ一つ、「副作用としてリフローを起こすこと」

つまり副作用だけを目的にした、意図的な式なんです。

このテクニックがトリッキーと言われる理由

このコード、実はCopilotちゃんに「トリッキー」と言われてしまいました😂
理由は以下のようです。

  1. 一見意味不明
    • パッと見ると「なんで値を捨ててるの?」と困惑する
    • 初見では絶対に理解できない謎のコード
  2. ブラウザの内部実装に依存
    • リフローのタイミングやブラウザの最適化の仕組みを理解している必要がある
    • まさに「ブラウザのクセを突いた」ハックテクニック
  3. 副作用を狙った処理
    • 普通は「副作用は避けるべき」なのに、ここでは副作用が目的という逆転の発想
    • 現場で困った開発者たちが編み出した「民間療法」的な解決策
  4. 公式ドキュメントにない知識
    • MDNには「アニメーション再発火のために使え」なんて書いてない

でも、だからこそ知っていると面白い!

そもそも「リフロー」とは?

リフロー(reflow)とは、ブラウザが DOM ツリーとスタイル情報をもとに、
要素のレイアウト(幅・高さ・位置)を再計算するプロセスのことです。

リフローが発生すると次のようなことが起こります。

  • CSSのトランジションやアニメーションがブラウザに正しく認識されやすくなる
  • スタイル変更の直後でも「変化あり」として描画が更新される

なぜ必要なのか?

NG例:同一フレーム内でのスタイル変更が最適化される

// アニメーションが再発火しない典型例
element.style.transform = 'translateX(0px)';
element.style.transform = 'translateX(100px)'; // 同一フレームで変更→最適化される
// CSSクラスでも同様の問題が起きる場合
element.classList.remove('fade-in');
element.classList.add('fade-in'); // 既にクラスがある場合など、最適化される場合がある

ブラウザは「最終的なスタイルだけ適用すればいい」と判断し、中間の変化を無視してしまいます。

OK例:void offsetHeight を挟むと動く

// 解決方法
element.style.transform = 'translateX(0px)';
void element.offsetHeight; // リフローを強制
element.style.transform = 'translateX(100px)'; // アニメーション発火!
element.classList.remove('fade-in');
void element.offsetHeight; // ← ここで強制的に再レイアウト
element.classList.add('fade-in'); // アニメーションが再発火!

なぜ void をつけるのか?

  • element.offsetHeightnumberを返すが、使わない
  • voidをつけることで「この値は不要、目的は副作用のみ」と明示
  • Linter(例:ESLint)の警告も回避でき、意図が伝わる

他の活用例

スタイルを一度リセットして再適用したいとき

element.style.opacity = '0';
void element.offsetHeight;
element.style.opacity = '1'; // opacity の transition が確実に走る

ウィンドウリサイズ後にアニメを再調整したいとき

window.addEventListener('resize', () => {
  adjustLayout();
  void element.offsetHeight;
  startTransition();
});

モーダルの開閉アニメーション

// モーダルを表示
modal.style.display = 'block';
void modal.offsetHeight; // レイアウトが確定するのを待つ
modal.classList.add('fade-in'); // フェードインアニメーション開始

代替手段とリフローを発生させる他のプロパティ

リフローを強制する方法は他にもあります。

レイアウト関連プロパティ

// 推奨(最も軽量でシンプル)
void element.offsetHeight;
void element.offsetWidth;

// 他のレイアウト関連プロパティ
void element.clientHeight;
void element.clientWidth;
void element.scrollHeight;
void element.scrollWidth;

// やや重いが同様にリフローを強制するもの
void element.getBoundingClientRect();
void element.getClientRects();
void window.getComputedStyle(element); // スタイル情報取得時に必要に応じて reflow

非同期的な解決方法

方法 説明
element.getBoundingClientRect() 同様にレイアウトを確定できる
requestAnimationFrame 内で次の処理 次フレームで確実に描画
setTimeout(..., 0) スタックを分けてレンダリングを挟める

今回ご紹介したvoid offsetHeightの強みは次の通りです。

  • 即時性(同期で効く)
  • 記述がシンプル
  • 意図が明確で読みやすい

注意点

  • 乱用はNG(リフローはコストが高い)
  • ループ内やスクロールイベント連打で使うとパフォーマンス悪化
  • あくまで 「ここぞという場面」 で使うピンポイントテクニック

多用は禁物!
リフローは重い処理なので、本当に困った時の最終兵器として使いましょう。

モダンブラウザでも必要?

結論:2025年現在でも必要な場面はある!
最新のChrome、Firefox、Safariでも、ブラウザの最適化エンジンは基本的に同じ仕組みで動いているため、この問題は現在でも発生します。

まだ必要な場面

  • React/Vue などのSPA(Single Page Application)での動的なクラス操作
  • styled-components や emotion などのCSS-in-JSライブラリ使用時
  • JavaScript で直接 element.style を変更する処理
  • 複雑なアニメーション連鎖や条件分岐のあるUI操作

不要になった/回避できる場面

  • 純粋なCSS Animation や CSS Transition(ブラウザが自動で最適化)
  • フレームワークが内部で適切にアニメーション管理してくれる場合
  • Web Animations API を使った現代的なアニメーション実装

使うべきタイミング

  • モーダルやドロワーの開閉アニメーションが動かない時
  • 動的にクラスを付け替えるUI操作で困った時
  • 「なぜかアニメーションしない」という原因不明の現象に遭遇した時

使わない方がいいタイミング

  • スクロールイベントやマウスムーブなど頻繁に発火する処理
  • ループ処理の中
  • 他の解決策がある場合

実際のデバッグ例

// デバッグ前:動かない
function toggleAnimation() {
  element.classList.toggle('animate');
  element.classList.toggle('animate'); // 結果的に元に戻る→何も起こらない
}

// デバッグ後:動く
function toggleAnimation() {
  element.classList.remove('animate');
  void element.offsetHeight; // 強制リフロー
  element.classList.add('animate'); // 確実にアニメーション発火
}

まとめ

void element.offsetHeight;

この一行は 「副作用だけを目的とした評価」 という意図のある小技です。
CSSアニメーションやスタイル操作で 「なぜか動かない」 とき、頼れる最終兵器になることもあります。

フロントエンド開発者なら一度は遭遇する「アニメーション動かない問題」。
ちょっとしたテクニックですが、知っているか知らないかで挙動が変わります。
UIの信頼性を上げたい人に、ぜひ使ってみてほしい一行です!

さいごに

この記事は業務中、コードレビューでこれって何ですか?と聞かれたので記事にしてみました。
最後まで読んでくださりありがとうございました!

9
0
0

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
9
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?