ヌルヌル動くReactコンポーネントの作り方【入門】

f:id:kaminashi-developer:20210126092859j:plain

こんにちは、カミナシの浦岡です。

最近、弊社のメンバーとしてUIデザイナーが新たに加わり、プロダクトのUI改善を進めています。

以前は、AntDesignなどUIライブラリーのコンポーネントをそのままプロダクトで使用する機会が多かったのですが、UI改善を行う上で、UIライブラリーそのままでは要件を満たすことが困難なケースも出てきました。

その結果、独自のReactコンポーネントを実装する機会が増えているのですが、 この記事では、その独自コンポーネントを「ヌルヌル動く」仕上がりにするために気をつけている点を架空の題材を使って書きます。

題材

今回、「空を舞うカレンダー」(ペルソナ5風!?)のUIがデザイナーから提示されたと仮定して進めます。

f:id:kaminashi-developer:20210124221452p:plain

極端な題材ですが、UIライブラリのカレンダーをベースには実現できそうにないので、独自コンポーネントとして作りましょう!

先に、ヌルヌル動かす上で気をつけている点をあげますが、下の2点だけです!

  • DOMを無駄に使わない
  • アニメーションはGPU使う

普段からレンダリンング速度を気にしながら開発しているエンジニアの方にとっては基本的な内容だと思いますが、レンダリング速度的なことはUIライブラリ任せにしているエンジニアの方を想定して書きます。

DOMを無駄に使わない

DOMに要素を大量に配置すると動作は重くなります。今回のカレンダーの場合、本日(2021年1月26日)〜9999年12月31日までの日数は約290万日。何も考えずに290万個のdiv要素をDOMに配置しようとすると、ブラウザは固まってしまいます。

仮想スクロール

上の問題を解決する方法として、仮想スクロールがあります。 簡単に説明すると、ブラウザ上に見えている範囲(+少しのバッファ)の要素だけDOM上に配置し、スクロールの移動に合わせてその要素を置き換えていく仕組みです。 スクロールがどれだけ進んでもDOM上の要素数を一定に保つことで、性能劣化を防ぎます。

仮想スクロールのライブラリは世にいくつも存在します。 その中から、今回はreact-windowを使ってみます。

github.com

import React from "react";
import { FixedSizeList as List } from "react-window";

export const Calendar = () => {
 // 今日を起点に未来しか見ない
 const startDate = new Date();

 return (
   <div className="calendar">
     <List
       layout="horizontal"
       direction="ltr"
       height={100}
       width={window.innerWidth}
       itemSize={100}
       // とりあえず、1000日分を指定(スクロールが進んだタイミングでこれを増やすことが可能
       itemCount={1000} 
     >
       {({ index, style }) => {
         return (
           <div key={index} style={style}>
           // 日付をレンダーする
           </div>
         );
       }}
     </List>
   </div>
 );
};

上の実装だけで今回作りたいカレンダーの大枠ができます 早速動かしてみましょう!

gyazo.com

スクロールでカレンダーの表示を進めても、DOM上の要素数が一定以内に抑えられているのがわかります。

アニメーションはGPU使う

今回のカレンダーの例では、画面中央付近の日付を両隅のものより大きく表示することにします。

カレンダーのスクロール中、react-windowのonScrollメソッドが発火するので ここを起点に、日付要素の画面位置を取得し、各日付要素の表示倍率を計算するようにします。

   <div className="calendar">
     <List
       onScroll={
            // スクロール中、ここが呼ばれる
       }
     >
            // ~略~
     </List>
   </div>

この時、注意しないといけないのが、このメソッドの発火頻度が約16msな点です。(実行ブラウザの仕様による) ここに遅い処理を実装してしまうとそれに引きずられて画面描画が遅くなります。 つまり、日付の拡大/縮小表示を高速に行う必要があります。

そもそも「遅い・早いの線引きは何?」となると思いますが、 ここでボトルネックになるのはJavaScript上の処理ではなく、大概がDOMのレンダリング処理です。 もっと言うとCPUでなくGPUレンダリングを使うようにすることで大概が解消します。

transform属性

対象要素のwidth/heightを変更するのではなく、transformのscaleで拡大するようにします。

<g transform={`scale(${props.zoom})`}>
    //~略~
</g>

これだけでGPUレンダリングされるようになり、CPUでのそれより高速に描画されます。

transform属性にはscale(拡大/縮小)以外にも移動(translate)、回転(rotate)などが指定できます。 https://developer.mozilla.org/ja/docs/Web/CSS/transform

結果

人間の目では識別できないくらいの速度になりました!

gyazo.com

ソースは以下

stackblitz.com

まとめ

以上、簡単な2点を注意するだけでヌルヌル動くReactコンポーネントができました!

実際問題、子コポンポーネント固有の性能問題に対処したり、イベント間引くなどのチューニングが必要になるケースもありますが、大枠としてはこの2点を注意することでヌルヌルが実現できると思います。ぜひ試してみてください!