各種ブラウザのフレームレートを調べた

ちょっと気になることがあって各主要ブラウザのフレームレートを調べてみたのでその結果をメモ。

調べ方

PCのChromeであれば、DevToolsで下記のようにリアルタイムのフレームレートを表示する事が可能です。

今回はChrome以外のブラウザや、スマホでも調べたかったので下記のようなコードを用意してみました。

requestAnimationFrame() を1秒間回して、その回数を調べる単純なコードです。

async function measureFPS() {
    return new Promise((resolve) => {
        let count = 0;
        const start = performance.now();
        function frame() {
            if (performance.now() - start >= 1000) {
                resolve(count);
                return;
            }

            count = count + 1;
            requestAnimationFrame(frame);
        }
        frame();
    });
}

それをHTML上に実装したものが こちらのページ で確認できます

Macに144Hzのディスプレイを繋いでChromeで試した結果がこちら

どうやら1〜2fpsほど大きい数字が出てしまうようです。
コードの問題かもしれませんが、まあいいでしょう。なんとなく測れればよいのです。

このHTMLを使って各ブラウザで計測していきます。

計測環境

結果発表の前に、計測した環境について書いておきます

  • Macbook Pro M1 Max
    - 内蔵ディスプレイ(ProMotionの120Hz)
    - 外部ディスプレイ:AG274UXP/11(144Hzで接続)
    - Sidecar の iPad Pro (ProMotion対応だが、Sidecar接続のため60Hz)

  • iPad Pro 11インチ 第4世代 M2チップ 
    - 内蔵ディスプレイ(ProMotion 120Hz)
    - 外部ディスプレイ:AG274UXP/11(ステージマネージャ有効)

  • Xiaomi 13T Pro
    - 内蔵ディスプレイ(最高144Hzまでの動的フレームレート設定)
    - 内蔵ディスプレイ(固定144Hz)
    - 内蔵ディスプレイ(固定60Hz)

AG274UXP/11が長いので以下AGONと表記します

計測した結果

Macbook Pro M1

ブラウザ ディスプレイ FPS
Chrome 内蔵 122
Chrome AGON 145
Chrome Sidecar 62
Safari 内蔵 62
Safari AGON 74
Safari Sidecar62
Firefox 内蔵 145
Firefox AGON 146
Firefox Sidecar 144

この結果から、Chromeはディスプレイのリフレッシュレートに合わせてレンダリングのフレームレート制限を可変させることがわかった。

Safariも基本的にはChromeと同じくディスプレイのリフレッシュレートを最大値としてフレームレート制限をかけるようだが、

  • 最高でも72FPS(?)までに制限される
  • ProMotionの下では60FPSに制限される

という条件がつくようだ。

Firefoxだけ理解できなかったのでAGONを物理的に切断して、MacとSidecarだけ繋がってる状態で再計測

ブラウザ ディスプレイ FPS
Firefox 内蔵 61
Firefox Sidecar(プライマリ) 62

なるほど、どうやらプライマリディスプレイのリフレッシュレートでレンダリングされるらしい。(↑の計測時はSidecarがプライマリになっていた)

今度は内蔵をプライマリにしてみる

ブラウザ ディスプレイ FPS
Firefox 内蔵(プライマリ) 120
Firefox Sidecar 120

なるほどね

iPad Pro 11インチ

ブラウザ ディスプレイ FPS
Safari 内蔵 61
Safari AGON 62

ステージマネージャの外部ディスプレイはブラウザの外側でリフレッシュレートがどのくらいなのか調べられなかったのだけど、マウスポインターをグルグル動かしたときの軌跡がProMotionな内蔵ディスプレイと比べると明らかに少ないので、おそらく60Hzで接続されてるのだと思う。

ちなみにAGONを接続せずにiPad単体で測っても62FPSでした。

Xiaomi 13T Pro

ブラウザ ディスプレイ FPS
Chrome 内蔵・動的Hz 122
Chrome 内蔵・固定144Hz 121
Chrome 内蔵・固定60Hz 62

Mac+Chromeと同じ結果を予想していたけど、144Hz固定にしても120相当しか出なかったのは予想外。この端末の特有な現象の可能性もある。

もしかしたらAndroidのChromeは上限120FPSで制限されているのかもしれない。

結論

今回iPhone実機では調査しなかったけど、まあ間違いなくiPadと同じで60FPSに制限されていると思います。

フロントエンド界隈では「ヌルサクアニメーションにするために60FPSを死守すべし!」というのはよく聞く話ですが、実際に調べてみた結果Safariをターゲットとするならばやはり60FPSがひとつの基準になるのは間違いないようです。
ただ、Safari以外はハードウェアさえ対応していれば少なくとも120FPSでレンダリングできるので、これからは120FPSを基準にしていくべきなのかもしれません。

iPhoneだってハードウェア的には120Hzなわけで、Apple的にハイリフレッシュレートに対して否定的な立場ではないハズ。
OS/Safariのアップデートで突然60FPS制限を引き上げるのも時間の問題なのでは?と思いました。

ColimaのDocker内で `composer install` に失敗する問題の解決

M1やM2などのArm Macで、かつ Colima でDockerを使っている場合、Docker環境内で composer install でするとコケる事がある。
厄介なのは、コケたりコケなかったりColimaごと固まったり非常に不安定なことがある。
その場合恐らく、デフォルトの qemu 環境で立ち上がっていると思われるので、一旦VM環境を削除した後に

colima start --vm-type vz --vz-rosetta --mount-type virtiofs

のようにVZ環境で作り直すとうまくいく。

Virtualization.framework(vz)はMac OS 13から利用できる仮想環境のAPIで、I/Oも早くなるらしい。

sass-loaderとdart-sassにまつわるfibersの話

tl;dr

用語について

現在npmで sass として公開されているパッケージはdart製のsassとなっています。旧来使われていたnode製のsassは node-sass として公開されています。

ここでは区別を分かりやすくするため、dart製のsassを dart-sass、 node製のsassを node-sass と呼び分けています。

本文

そもそもsass-loaderで何を設定しているのでしょうか。sass-loaderでdart-sassを利用する際はだいたい下記のように設定しているかと思います。

// webpack.config.js
module.exports = {
  // ...
  module: {
    rules: [
      {
        // ...
        use: [
          `// ...
          {
            loader: `sass-loader`,
            options: {
              implementation: require('sass'), // ここで `dart-sass` を読み込んでいる
              sassOptions: {
                fiber: require(`fibers`), // 大体の人がなんとなく一緒に読み込んでいる
              },
            },
          },
        ],
      },
    ],
  },
  // ...
};

まず implementation プロパティについてはsass-loaderのドキュメント に書いてあるとおりですが簡単にまとめると

  • implementation を設定しない場合、 node-sassとdart-sassの両方インストールされている場合、自動的に後者が読み込まれる
  • implementation を設定することで明示することができる

の2点かとおもいます。この「両方がインストールされている場合」というのは、プロジェクト内の package.jsondevDependencies などに追加されている場合以外にも他のパッケージの依存に入っている場合も含まれるので、基本的には明示したほうが良いでしょう。

では、なぜ dart-sass を利用する場合に fiber プロパティにfibersパッケージを指定しているのでしょうか。

node-sassにもdart-sassにも render()renderSync() というAPIがあります。これはどちらもscssからcssへコンパイルするための関数ですが、前者が非同期に実行され、後者は同期的に実行されます。sass-loaderでは render() が利用され非同期的にscssファイルがコンパイルされることになります。

しかし、 dart-sassでは非同期にに実行する render() が非同期実行のオーバーヘッドのため renderSync() より2倍近く遅くなってしまいます。これを避けるために、利用されるのがfibresです。

fibersはもともとnode.jsに非同期に関数を実行する仕組み(Promiseやasnyc/await)がなかった頃に開発されたものです。同期関数を非同期で実行することができる Promise に相当する機能があります。(他にもGeneratorに相当する機能もありますがここでは割愛します。)

速度の早い renderSync() 関数を非同期でするために、このfibersを利用する設定が前述した webpack.config.js です。ただしfibersは作者により使用を避けるべきとされており、node 16への対応もされていません。

まとめ

つまり記事の冒頭でまとめたように、 node 16 を利用するのであればfibersを利用することはできず、コンパイル速度の低下を受け入れざるを得ません。sass-loaderではdart-sassの場合に renderSync() を利用するIssueが立っているのでもしかするとsass-loader側で対応がなされるかもしれません。

nvmとかnodenvとかanyenvをやめてasdfにした

これまでnode環境はnvmで、他はpyenvやgoenvを利用していたのですが、nvmが .node-version ファイルに対応していませんでした。(.nvmrc で指定することはできませす。)
そこでいい加減nodenvに移行したのですが、せっかくなのでどうせなら新しい物をを使おうと思いasdfに移行しました。

anyenvの問題点ははただの「**envをインストールソフト」であり、nodenvやpyenvなどそれぞれの利用法を知っている必要があります。
asdfは実行環境ごとのプラグインを介して直接バージョンを管理するため
どの実行環境でも同様に扱うことができます。
ある日突然「PHPの5.3のバイナリが欲しい…」となっても使い方を迷う必要がありません。

例としてnodejsであれば

asdf install nodejs 16.1.0

Pythonであれば

asdf install python 3.9.5

でインストールが可能です。

また、nodejsであれば lts-fermium のようにタグで指定することも可能です。
自分はグローバルにインストールするものは「一番新しいLTSの最新バージョン」を利用することにしているので、非常に便利です。(nodenvではこれができませんでした。)
また、.asdfrclegacy_version_file = yes を追加することで .node-version なども自動的に読み込み、ディレクトリによって自動的にバージョンを切り替えることもできます。

注意点としては、ビルドする関係上、使用するプラグイン(実行環境)によって依存するライブラリがあるため、そのインストールは別途必要になります。

anyenvより圧倒的に使いやすいので、anyenvやその他のバージョン管理ツールを利用している場合は、asdfへの移行を強くおすすめします。

movファイルを手軽にgifに変換するシェル関数

パッと画面上の動きなどをSlackなどで共有したい場合に、mov形式の動画からGIFを生成するのは何かと面倒ですし、ファイルサイズも大きくなりがちです。かといってAppStoreから余計なフリーソフトなどを入れたくないですよね。そこでシェル関数という形でまとめてみました。

1. FFmpegとgifsicleをインストール

brew install ffmpeg
brew install gifsicle

などする。FFmpegは言わずと知れた動画や画像を変換するためのソフトウェアで、gifsicleはアニメーションGIF化をいい感じにやってくれるやつです。

2. 下記のシェルスクリプトを.zshrcなどにコピペする

function mov2gif() {
	mov=$1;
	if [[ -z $2 ]]; then
		width=300
	else
		width=$2
	fi
	if [[ -z $3 ]]; then
		rate=15
	else
		rate=$3
	fi
	gif=`basename $mov`".gif"
	ffmpeg -i $mov -vf scale=$width:-1 -pix_fmt rgb24 -r $rate -f gif - | gifsicle --optimize=3 --delay=3 > $gif
}

3. movファイルを変換する

mov2gif hoge.mov #オプション指定なしだと横幅300px・FPS15のGIFを出力
mov2gif hoge.mov 500 30 #と指定すれば横幅500px・FPS30で出力

アニメーションGIFのファイルサイズはFPSに大きく依存しますが、動きをよく見せたい場合もあるので、FPSを指定できるようにしてあります。
またGIF画像の高さを指定できないのは、殆どの場合キャプチャした動画は正方形に近く、サッと共有したい場合にはだいたいの画像の大きさが指定できれば十分という理由からです。

出力例

こんな感じのGIFファイルが生成できます。(横幅500pxで出力)

PhpStormでnode製サイトのプレビューをする

node製サイトを開発時はwebpackを利用していればwebpack-dev-serverなどを利用するのが常ですが、プロダクションビルド後のファイルで確認したい場合もあります。

そういうときにわざわざサーバーにアップロードしたり、MAMPなどを利用するのは面倒です。ですが、PhpStormであればビルドも含めてショートカット1発でサーバーを起動することができます!

今日はその設定をしていきましょう。設定は1分で終わります。

Step.1 サーバー設定

「Run」->「Edit Configurations」から「Run/Dubug Configurations」のウィンドウを表示し下記に設定します。

  • Name: Preview server(好きな名前を設定してください)
  • Single instance only: チェックを入れる(重複起動を許可しないかのチェックです)
  • Host: localhost
  • Port: 3001
  • Document Root: ドキュメントルートへのパス

HostとPortは他のアプリケーションが利用中だと起動できないので、webpack-dev-serverやMAMPなどを利用している場合は被らないように設定するのが吉です。

Step.2 npmスクリプトの起動設定

次に「Before launch: Acvtive tool window」で「+」をクリックして「Run npm script」をクリックします。

すると「NPM Script」のウィンドウが開くので下記に設定します。

  • Command: run
  • Scripts: build(など自分でpackage.jsonに設定したスクリプト名)

Step.3 起動

あとは^ Rまたは「Run」->「Run 'Preview server'(上記のName:で設定した名前)」をクリックすれば指定したHostとPortでサーバーが起動します。

PhpStormなどJetBrains製のIDEはWeb関係の技術と親和性が高いので、Web開発者は必携ですね。