Forced Synchronous Layout を意図的に起こす

今日は JavaScriptで、Forced Synchronous Layout (強制リフロー)を意図的に起こす場合を解説します。Forced Synchronous Layout(強制リフロー)については以前の記事 で解説しているのでよければそちらも合わせて御覧ください。

単刀直入ですが、Forced Synchronous Layout (強制リフロー)を意図的に起こす場合とは、CSS Transition によるアニメーションを1度の Script 処理で起こしたい場合です。

具体的にどういうことかというと、CSSアニメーションの仕様上、1度のScript処理の中で JavaScript によって CSS の値を変化させると、Layout処理は JavaScript の処理が終わったあとにしか起きないため、アニメーションが起こりません。そのため、JavaScriptの処理を行っている間に強制的ににブラウザのレンダリングエンジンにレイアウト計算をさせて上げる必要があります。レンダリングエンジンがレイアウト計算を行うことで、ブラウザはCSSプロパティの値が変化したことを検知しアニメーションが動作するという理屈になります。

例えば、以下の JavaScript を見てみましょう。

const slideUp = (el, duration = 300) => {
  const { style, offsetHeight } = el;
  style.height = `${offsetHeight}px`;
  style.transitionProperty = "height, margin, padding";
  style.transitionDuration = `${duration}ms`;
  style.transitionTimingFunction = "ease";
  style.boxSizing = "border-box";
  style.overflow = "hidden";
  style.height = 0;
  style.paddingTop = 0;
  style.paddingBottom = 0;
  style.marginTop = 0;
  style.marginBottom = 0;
  setTimeout(() => {
    style.display = "none";
    style.removeProperty("height");
    style.removeProperty("padding-top");
    style.removeProperty("padding-bottom");
    style.removeProperty("margin-top");
    style.removeProperty("margin-bottom");
    style.removeProperty("overflow");
    style.removeProperty("transition-duration");
    style.removeProperty("transition-property");
    style.removeProperty("transition-timing-function");
  }, duration);
};

上記のソースコードは、jQueryの slideUp メソッドを素の JavaScript で再現した関数です。しかし、この関数を実行してみるとスライドのアニメーションは動きません。なぜかというと、ソースコードの3行目と9行目で height プロパティの値を変更していますが、その間にブラウザのレンダリングエンジンがレイアウト計算を行っていないため、ブラウザのレンダリングエンジンにとっては 要素の高さが1度しか更新されていないと判断してアニメーションが引き起こされないという現象が起こります。

上記の slideUp 関数で意図通りにスライドのアニメーションを起こすには 3行目と9行目の間でレイアウト情報を参照します。具体的には offsetHeight プロパティを参照します。

修正したソースコードが以下になります。

const slideUp = (el, duration = 300) => {
  const { style, offsetHeight } = el;
  style.height = `${offsetHeight}px`;
  el.offsetHeight; // ←ここでレイアウト情報を参照します。
  style.transitionProperty = "height, margin, padding";
  style.transitionDuration = `${duration}ms`;
  style.transitionTimingFunction = "ease";
  style.boxSizing = "border-box";
  style.overflow = "hidden";
  style.height = 0;
  style.paddingTop = 0;
  style.paddingBottom = 0;
  style.marginTop = 0;
  style.marginBottom = 0;
  setTimeout(() => {
    style.display = "none";
    style.removeProperty("height");
    style.removeProperty("padding-top");
    style.removeProperty("padding-bottom");
    style.removeProperty("margin-top");
    style.removeProperty("margin-bottom");
    style.removeProperty("overflow");
    style.removeProperty("transition-duration");
    style.removeProperty("transition-property");
    style.removeProperty("transition-timing-function");
  }, duration);
};

codesandboxに実際にソースコードが動いている環境を用意しました。是非、 offsetHeight をコメントアウトしてみたりして実験してみてください。

補足として、offsetHeight だけでなく、レイアウトの情報を返すメソッドやプロパティである、offsetWidthoffsetTopclientHeightgetComputedStyle() でレイアウト情報の参照を行った場合でも同様の現象が起きました。

まとめ

今回は Forced Synchronous Layout(強制リフロー)を意図的に起こすことで、アニメーションを実装することができることを記事にしました。これ個人的には結構ハマったのですが、ブラウザの仕様をきちんと整理して考えていったらきちんと答えにたどり着くことができました。 Forced Synchronous Layout(強制リフロー)についてはパフォーマンス的に良くない処理として言われているだけにそれを逆に利用するという発想に転換するのには苦労しましたが…。


関連記事

この記事のハッシュタグに関連する記事が見つかりませんでした。

最新記事

カテゴリー

アーカイブ

ハッシュタグ