♪Baby if I could change the world (c) Eric Clapton

CSS will-changeはウェブの世界を変えうるのか?

CSSのwill-changeがまもなくFirefoxでデフォルトで有効になりそうな感じの昨今。

このwill-changeというのは、ざっくり言うと、アニメーションなどの変化を事前にブラウザに通知するというもので、その通知を受け取ってブラウザ側は変化に備えてあらかじめ最適化を行う、というものである。

つまり、「通知を行うことだけが役割」という妙なプロパティである。

その一方でwill-changeはかなり「取扱注意」なプロパティでもある。 W3Cの仕様などではブラウザに必要最小限な時間を与えつつアニメーションなどの変化を終えたら直ちにwill-changeを初期値に戻せ、と注意書きがされている。 さらに、むやみやたらとwill-changeを使ったりずっとwill-changeを有効にしておくと、却ってパフォーマンスが落ちて、最悪、クラッシュするぞ、と脅かしてもいる。

そうかと思えば、指定したところでブラウザに通知を無視されるかもしれないよ、ともあったりする。

こんなプロパティが登場した背景には、アニメーションが滑らかになる、ということでtranslateZ(0)を使ったハックなどが蔓延している事情があるんだそうな(うちでは使ってないけど)。 translateZ()の本来の目的から外れた使い方はやめて、will-changeを使えと。

つまり、使い勝手はあまりよくなさそうだし存在意義もよくわからない感があるこのwill-changeだが、実は、ハックと称したバッドノウハウだらけの今のウェブの世界を変える、という重大な使命を担っているわけである。

でも、ChromeとFirefoxは実装済み(Firefoxは今はabout:configから有効にする必要がある)だが、IE11は'Under Consideration'でどうなるかわからず、Safariに至ってはWebkitのBugzillaを検索しても該当バグが見当たらない。 なので、結局、何も変わらないかもしれない…orz

will-change用の汎用スクリプト (最終更新:2016/12/25)

それはさておき、will-changeは、CSSのプロパティでありつつもスタイルシートに記述するのではなく(書いてもいいけど)、JavaScriptなどからの操作が(ほぼ)「前提」となっている。

そこで、will-changeを汎用的に使えるスクリプトを考えてみた。


/**
 * toggleWillChange
 * CSS will-change用の汎用スクリプト
 * Copyright (c) 2014-2016 Kazz
 * http://asamuzak.jp
 * Dual licensed under MIT or GPL
 * http://asamuzak.jp/license
 * @param {Element} o - will-changeを適用させる要素
 * @param {string} p - will-changeに指定するプロパティのリスト
 * @param {?Array} q - will-changeのキューとなるイベント名の配列
 *                     省略またはnullの場合はmouseenterなどに適用
 *                     配列[0]に適用イベント名、配列[1]にリセットのイベント名
 *                     例)["click","dblclick"]
 * @param {?Element} r - will-change指定のコントローラとなる要素
 *                       省略またはnullの場合はoの要素自身
 * @param {?string}  s - will-changeを最終的にリセットさせるアニメーション名
 *                       省略時はanimationend毎
 */

(function(_win, _doc) {
  "use strict";
  function isType(o, t) {
    var x = o && Object.prototype.toString.call(o).slice(8, -1);
    return o && (t ? new RegExp("^" + t + "$", "i").test(x) : x);
  }
  function isStringOrEmptyString(o) {
    return /^(?:String)?$/i.test(isType(o));
  }
  function isElement(o) {
    return /HTML(?:[A-Z][a-zA-Z]+)?Element/.test(isType(o));
  }
  function toggleWillChange(o, p, q, r, s) {
    var x = _win.CSS && _win.CSS.supports("will-change", "auto") && "auto";
    function setWillChange() {
      x === "auto" && (
        o.style.setProperty("will-change", p, ""),
        x = p
      );
    }
    function resetWillChange(evt) {
      x && x !== "auto" && (/animationend/.test(evt.type) ?
        !s || s === evt.animationName :
        _win.getComputedStyle(o, "").animationName === "none"
      ) && (
        o.style.removeProperty("will-change"),
        x = "auto"
      );
    }
    if(x && isElement(o) && isType(p, "String")) {
      var y = isElement(r) ? r : o;
      if(q && Array.isArray(q)) {
        var z = q[0];
        if(isStringOrEmptyString(z)) {
          switch(z) {
            case "":
              setWillChange();
              break;
            case "load":
              _win.addEventListener(z, setWillChange, false);
              break;
            default:
              /^[a-z]+$/i.test(z) &&
                y.addEventListener(z, setWillChange, false);
          }
        }
        (z = q[1]) && isType(z, "String") && /^[a-z]+$/i.test(z) &&
          y.addEventListener(z, resetWillChange, false);
      }
      else {
        _win.PointerEvent ? (
          y.addEventListener("pointerenter", setWillChange, false),
          y.addEventListener("pointerleave", resetWillChange, false)
        ) : _win.TouchEvent ? (
          y.addEventListener("touchstart", setWillChange, false),
          y.addEventListener("touchend", resetWillChange, false)
        ) : _win.MouseEvent && (
          y.addEventListener("mouseenter", setWillChange, false),
          y.addEventListener("mouseleave", resetWillChange, false)
        );
      }
      o.addEventListener("animationend", resetWillChange, false);
    }
  }

  /* DOMContentLoadedのタイミングで実行 */
  _doc.addEventListener("DOMContentLoaded", function() {
    toggleWillChange(/*引数を指定*/);
    toggleWillChange(/*引数を指定*/);
  }, false);
})(window, document);

そして、そのテストケース
ただし、will-changeを実装済みのChromeやOpera、will-changeを有効にしたFirefoxで閲覧しないと意味はないが、それらで見たところで明確な違いは見いだせないかもしれないのでご了承をw

実際にうちのサイトに取り込んでみたところ、とくにオープニングのアニメーションで効果が見られ、モバイルでも駒飛びやチラツキがなくなった(…ような気がするw)。 なので、イニシャルをフリップさせるアニメーションをモバイルでも有効にすることにした。

ということで、will-changeは少なくともうちのサイトは変えたw

"♪Baby if I could change the world (c) Eric Clapton"へのTwitter上でのコメントやRT

6件のツイートがあります。

ツイート 1

ツイート 2

ツイート 3

ツイート 4

ツイート 5

ツイート 6