無料でJavaScriptによるウェブアプリ構築のデザイン・レンダリング・パフォーマンスパターンがわかる「Patterns.dev」

近年は特にウェブ開発に関するエコシステムの移り変わりが激しく、様々な手法が取り入れられたり廃れたりしています。そんな中で安定して使えるパターンを身につけたいと考える開発者も多いはず。そんなニーズに応えるべく登場したのが「Patterns.dev」という無料のオンラインリソース集です。Vanilla JavaScriptや最新のフレームワークで強力なウェブアプリを構築するために必要となるデザインパターン・レンダリングパターン・パフォーマンスパターンを扱っています。
Patterns.dev
https://www.patterns.dev/

「Patterns.dev」では、大きなテーマとして以下の3つを扱っています。
・Vanilla JavaScript
・React
・Vue.js
それぞれのテーマにおいて、さらに以下の3つのカテゴリに分かれています。
・デザインパターン
・レンダリングパターン
・パフォーマンスパターン(※記事作成時点ではReactには存在しない)
今回はこのうちVanilla JavaScriptを例に、取り上げられているパターンをいくつか見ていきます。なお、「Vanilla JavaScript」とは、jQueryやReactといった強力な外部ライブラリーを使用しない、素の状態に近いJavaScriptを指す言葉です。
◆デザインパターン
ソフトウェア設計において頻繁に発生する問題に対する典型的な解決策を提供することができるので、デザインパターンはソフトウェア開発においては必須要素といえます。つまり、特定のソフトウェア部品を提供するものというよりは、繰り返し発生するテーマを最適な方法で処理するための概念がデザインパターンである、と捉えるのが適切です。
・シングルトン・パターン

シングルトンとは、唯一のインスタンスを生成するとそれを使い回し、アプリケーション全体からアクセス可能とするオブジェクトを指します。シングルトン・パターンを用いると、グローバルな状態管理やリソースの共有が容易になります。ただし、シングルトン・パターンは過度に使用するとコードの可読性が落ち、テストの難易度が上がるため、使いどころには注意が必要です。
・プロキシー・パターン

プロキシー・サーバーという用語でも使用されているように、プロキシーとは「代理人」を意味し、あるオブジェクトとのアクセスを代理で行うオブジェクトをプロキシーオブジェクトと呼びます。プロキシーオブジェクトは検証機能を追加するのに便利であり、またフォーマットの調整やデバッグにも役立ちます。一方、ターゲットとなるオブジェクトに対してダイレクトにアクセスする場合と比べるとワンクッションを置く分どうしてもパフォーマンスが低下するため、高パフォーマンスを求める場合には使用を控えるべきです。
・プロトタイプ・パターン

プロトタイプ・パターンは、ES2015(ES6)のクラスを使用してオブジェクトのプロトタイプを定義し、新しいオブジェクトをそのプロトタイプから生成するパターンです。

さらに、継承によってサブクラスを定義すると、既存のクラスが持つ機能をすべて継承しつつ、新たな機能を追加することも可能です。プロトタイプ・パターンを使用すると、オブジェクトが他のオブジェクトのプロパティにアクセスしたり継承したりすることが容易になります。プロトタイプチェーンによりオブジェクト自体に直接定義されていないプロパティにもアクセスできるため、メソッドやプロパティの重複定義を避けることができ、メモリ使用量の削減にも寄与します。

・オブザーバー・パターン

あるオブジェクト群(オブザーバー)を別のオブジェクト(オブザーバブル)に登録し、オブザーバブルの状態が変化した際にオブザーバーに通知を送る仕組みを提供するのがオブザーバー・パターンです。オブザーバー・パターンを使用すると、コンポーネント間の疎結合な通信が可能となり、コードの可読性と保守性が向上します。一方で、オブザーバーが複雑になりすぎるとパフォーマンスに悪影響を及ぼす可能性もあるため、過度な使用には注意が必要です。
・モジュール・パターン

アプリケーションの機能を独立したモジュールに分割し、各モジュールが独自のスコープを持つようにするのがモジュール・パターンです。コードの再利用性と保守性が向上し、グローバル名前空間の汚染を防ぐことができます。ES2015以降では、JavaScriptモジュールが標準でサポートされており、import/export構文を使用してモジュールを簡単に管理できます。
・ミックスイン・パターン

ES2015以前のJavaScriptでは、継承がサポートされていなかったため、別のクラスから機能を組み合わせる手法としてミックスイン・パターンが用いられてきました。ミックスイン・パターンを使用すると、コードの再利用性が向上し、異なるクラス間で共通の機能を共有することが容易になります。
class Dog {
constructor(name) {
this.name = name;
}
}
const dogFunctionality = {
bark: () => console.log("Woof!"),
wagTail: () => console.log("Wagging my tail!"),
play: () => console.log("Playing!"),
};
Object.assign(Dog.prototype, dogFunctionality);
ただし、オブジェクトのプロトタイプを直接変更することは不具合を招く要因となる場合があり、現在ではES2015のクラス構文を使用することが推奨されています。
・メディエーター/ミドルウェア・パターン

アプリケーションを構成するコンポーネントが多い場合、コンポーネント同士で直接通信を行うと、依存関係が複雑化し、保守性が低下してしまいます。

メディエーター・パターンは、コンポーネント間の通信を仲介するオブジェクトとしてメディエーターを導入することで、コンポーネント同士の直接的な依存関係を排除し、疎結合な設計を実現するパターンです。コードの可読性と保守性が向上し、変更が容易になります。

・フライウェイト・パターン

多数のオブジェクトを生成する必要がある場合、同じデータや状態を1つのオブジェクトとして共有するのがフライウェイト・パターンです。
const createBook = (title, author, isbn) => {
const existingBook = books.has(isbn);
if (existingBook) {
return books.get(isbn);
}
const book = new Book(title, author, isbn);
books.set(isbn, book);
return book;
};
フライウェイト・パターンを使用すると、メモリ効率が改善され、アプリケーションのパフォーマンスが向上します。
・ファクトリー・パターン

キーワードnewを使用せずにオブジェクトを生成する「ファクトリー関数」を使用するのがファクトリー・パターンです。以下はファクトリー関数の一例です。
const createUser = ({ firstName, lastName, email }) => ({
firstName,
lastName,
email,
fullName() {
return `${this.firstName} ${this.lastName}`;
},
});
ファクトリーパターンは、同じプロパティを共有する複数の小さなオブジェクトを作成する必要がある場合に便利です。一方で、newキーワードを使用してクラスのインスタンスを生成する場合と比べてパフォーマンスが低下する可能性があるため、使用する際には注意が必要です。
◆レンダリングパターン
新しいウェブアプリの設計を始める際に行うべき基本的な決定の一つは、「コンテンツをどのように、どこでレンダリングするか」です。画一的な回答はなく、ユースケースに応じて最適な方法を選択する必要があります。適切なパターンを選択すれば、ビルド速度の向上や優れた読み込みパフォーマンスを実現できます。一方で、パターンを間違えると、素晴らしいビジネスアイデアを実現できたはずのアプリが台無しになってしまう可能性さえあります。

・アイランド・アーキテクチャー
JavaScriptを過剰に使用するとパフォーマンスが低下する恐れがありますが、ウェブサイトを構築する上である程度のインタラクティブ性を求められることが多く、静的なコンテンツであってもJavaScriptが必要になります。アイランド・アーキテクチャーは、必要な部分にのみJavaScriptを適用することで、これらの問題を解決します。アイランド・アーキテクチャーでは、静的なHTMLコンテンツを持つコンポーネントを中心に据えつつ、インタラクティブな要素を持つコンポーネントだけをJavaScriptでハイドレートします。コンポーネントごとにハイドレーションスクリプトを持ち、コンポーネント単位で非同期処理が行われるため、あるコンポーネントでパフォーマンスが低下したとしても、他のコンポーネントに影響を与えることはありません。

・ビュー遷移のアニメーション

View Transitions APIは、視覚的なDOMの変化をある状態から別の状態へと遷移させるシンプルな手段を提供します。APIが提供する遷移については、コンテンツを切り換えるような単純なものから、あるページから別のページへの移動といった大掛かりなものまで、様々なものを含みます。遷移にあたって注意すべきなのは、まずは画面が操作不可能な状態になる時間を最小限に抑えること、さらにはDOMを更新する前に初期状態のDOMのスクリーンショットがキャプチャされていることが挙げられます。
◆パフォーマンスパターン
ウェブページの読み込みを成功させ、スムーズな体験を実現するには、重要なコンポーネントとリソースを適切なタイミングで利用できるようにする必要があります。これがうまくできているかどうかで、ユーザーはウェブページのパフォーマンスが優れているかどうかを判断します。
実のところ、最適な読み込み順序の確立というのは困難なものであり、多くの場合、開発者が期待する順序とブラウザが優先するリソースの読み込み順に食い違いが発生することが原因となります。リソースの種別による優先順は概ね以下の通りとなります。
・クリティカルCSS:FCPに必要な最小限のCSS。HTMLにインラインで記述するのが適切です。
・クリティカルフォント:クリティカルCSSと同様にインラインで記述するのが適切。それが無理なら「preconnect」を指定してスクリプトを読み込む必要があります。
・ATF画像:サイズ調整が必要、未調整の画像はCLSメトリックに悪影響を与えます。
・BTF画像:遅延読み込みの対象として最適です。
・ファーストパーティーJavaScript:ネットワーク上ではATF画像よりも先に読み込みを開始し、メインスレッドではサードパーティーJavaScriptよりも先に実行する必要があります。
・サードパーティーJavaScript:ブロッキングや遅延の原因となりやすいため、より適切な制御が必要となります。
・静的インポート

ES2015のインポート構文を使用すると、別のモジュールによってエクスポートされたコードを静的にインポートすることができます。読み込み時間を短縮するためには、静的インポートするモジュールは厳選すべきです。
・動的インポート

インタラクティブ要素を有するコンポーネントに関するモジュールを静的にインポートすると、インタラクティブなイベントが発生しない場合には読み込みが無駄になる場合があります。動的インポートを行うと、必要に応じてインポートを実行できるため、初期読み込み時間の短縮に繋がります。
・可視性に基づくインポート

Intersection Observer APIなどを使用して、画像などの要素がビューポートに入ったときにのみ読み込むようにする「遅延読み込み」が代表例です。ユーザーが実際に必要とするコンテンツのみが読み込まれるため、パフォーマンスの最適化に寄与します。
・インタラクション時のインポート

一部のユーザーインターフェースはインタラクションが発生するまで必要とされない場合があります。例えば、モーダルウィンドウやドロップダウンメニューなどです。これらのコンポーネントに関連するモジュールをインタラクションが発生した際にのみインポートすることで、初期読み込み時間を短縮できます。具体的な読み込みタイミングの例としては、以下が考えられます。
・ユーザーが初めてそのコンポーネントをクリックして操作したとき
・ユーザーがコンポーネントをスクロールして表示したとき
・ブラウザがアイドル状態になったとき
最も単純なダウンロードシナリオとして、単純なクライアントサイドレンダリング(CSR)を使用している場合を想定すると、HTML・JavaScriptコード・CSS・データといったリソースがダウンロードされ、すべてが揃った時点でようやくレンダリングされます。言い換えると、すべてのリソースがダウンロードされるまでの間、ユーザーは何も見ることができないわけです。

次に、このエクスペリエンスがサーバーサイドレンダリング(SSR)に移行した場合を想定してみます。SSRでは、サーバーが最初のHTMLを生成し、クライアントに送信します。これにより、ユーザーはすぐにコンテンツを閲覧できるようになります。ただし、サーバーからデータを取得し、クライアントフレームワークがハイドレーションを完了するまでは、インタラクティブな要素は機能しません。この時、ユーザーは一種の「不気味の谷現象」に陥る可能性があります。つまり、一見するとページは完全に読み込まれたように見えるものの、実際にはインタラクティブな要素が機能していないため、ユーザーは混乱してしまうわけです。その結果、ユーザーがイライラして何度もクリックしてしまう「レイジクリック」と呼ばれる行動をとる可能性が高まります。

インタラクション駆動型の遅延読み込みを使用すると、ユーザーが実際に必要とするコンテンツのみが読み込まれるため、パフォーマンスの最適化に寄与します。例えば、まず画面を構成するための最小限のコードをダウンロードし、ページの見た目を完成させます。次に、ユーザーが特定の操作を行ったときにのみ、その操作に関連するモジュールをインポートするように設定できます。これにより、初期読み込み時間が短縮され、ユーザーエクスペリエンスが向上します。

「Patterns.dev」には、今回の記事で取り上げた以外にも多くのパターンが掲載されており、またReactやVue.jsといったフレームワークに特化したパターンも解説しているので、興味のある人はぜひチェックしてみてください。
・関連記事
FirebaseのようにGUIでバックエンドを構築できる無料でオープンソースのシステム「PocketBase」、わずか1ファイルのみ - GIGAZINE
初代Macintoshに搭載された計算機のデザインはスティーブ・ジョブズが設計した - GIGAZINE
VLCの開発者が手がける超低遅延動画ストリーミングを可能にするオープンソースキット「Kyber」とは? - GIGAZINE
プラットフォームの上に劣化版のプラットフォームを作成してしまうアンチパターン「内部プラットフォーム効果」とはどういうものなのか - GIGAZINE
プログラマーの目線から見たGoogleの失敗・不可解な取り組み41選 - GIGAZINE
「良いコード」を書くための10のポイントとは? - GIGAZINE
・関連コンテンツ
in 教育, ソフトウェア, ウェブアプリ, Posted by log1c_sh
You can read the machine translated English article 'Patterns.dev' - Learn design, rendering….







