イベント処理におけるオブザーバーとイベントハンドラーの違いを深掘りしてみよう!

frontendbasefundamentalbasicobserver-vs-event-handler

概要

オブザーバーとイベントハンドラーの違いは何だろう?
オブザーバーとイベントハンドラーの違いは何だろう?

最近オブザーバーとイベントハンドラーの違いについてあまり気にしていなかったのですが、最近大きく学ぶことがありました。

ただ、曖昧な部分があったので、この機会に改めて整理しようと記事を書くことにしました。

始める前に...

一度考えてみてはいかがでしょうか。イベントハンドラーはたくさん使ったことがあると思いますが。

それでは、オブザーバーはいつ使ったことがありますか?

  • 無限スクロールを実装する時に使うIntersection Observer?
  • 画面のサイズ変更を検知する時に使うResize Observer?

それでは、皆さんはなぜこのようなオブザーバーを使いましたか?

Event Handlerを使った時と比べてどんな利点がありましたか?

一度考えてから深掘りしてみましょう!

オブザーバー(Observer)とは何か?

皆さん、オブザーバーとは何でしょうか?

どこかで聞いたことはありませんか?

スタークラフトのオブザーバー
スタークラフトのオブザーバー

スタークラフトで聞いたことがあれば...大体近いところまで来ています笑。

オブザーバーは文字通り「観察者(Observer)」です。

正確には「オブザーバーパターン(Observer Pattern)」に基づいたメカニズムです。

それでは、ここで何を観察するのでしょうか?

答えは特定の対象の状態変化を観察します。

特定の対象の状態変化を継続的に観察し、変化が検知されると事前に登録されたコールバック関数を実行する方式で動作するのがオブザーバーです。

オブザーバーの核心的な特徴は非同期的で継続的な観察です。

一度登録すると条件が満たされるたびに自動的にコールバックが実行され、開発者が直接状態を確認する必要がありません。

まるでセキュリティカメラが動きを検知すると自動的に記録するようなものです。

一度IntersectionObserverを例に挙げてみましょうか?

JavaScript

上記のコードをたくさん使ったことがあると思います。特定の要素が画面上で検知されると、コールバックを実行するコードです。

ここで次の部分が見えますか?

JavaScript

オブザーバーに何かを観察するように指示しています。ここでは選択された要素の状態変化を検知すると言えますね。

IntersectionObserverはDOM要素がビューポートや他の要素と交差する状況を観察します。

スクロールしたり、要素の位置が変更されるたびに自動的に交差状態を確認し、条件に合えばコールバックを実行する方式です。

理解できなくても大丈夫です。こういうものがあるという程度だけ見ておきましょう笑。

イベントハンドラー(Event Handler)とは何か?

イベントハンドラーはイベント駆動プログラミングの核心要素です。

ユーザーのクリック、キーボード入力、マウスの動きなど特定のイベントが発生した時即座に反応してコールバック関数を実行します。

ここでの核心は「即座に」です。これに注目してください。

イベントハンドラーの特徴は即時的で反応的な実行です。

イベントが発生する瞬間すぐにコールバックが実行され、イベントの詳細情報(どのキーを押したか、どこをクリックしたかなど)をコールバック関数に渡します。

JavaScript

イベントハンドラーについてはよく知っていると思うので簡単に触れて進みます笑。

イベントハンドラーとオブザーバーの違い

オブザーバー vs イベントハンドラー コールバック実行方式の比較

区分オブザーバー (Observer)イベントハンドラー (Event Handler)
実行タイミングブラウザ最適化サイクルに従って実行
レンダリングフレームと同期
イベント発生時即座に実行
ユーザーアクションと直接連結
実行頻度ブラウザが調整 (throttling内蔵)
重複呼び出し自動最適化
イベント発生回数と1:1対応
速い連続イベント時に過度な呼び出し可能
コールバックキュー処理Intersection Observer Task Queue
低優先度で処理
Event LoopのTask Queue
高優先度で即座に処理
メインスレッドブロッキングノンブロッキング方式
メインスレッド性能への影響最小
ブロッキング可能性
複雑なコールバック時にUIレスポンス低下
バッチ処理複数の変化をバッチでまとめて処理
entries配列で渡される
個別イベントごとに別途処理
各イベントは独立的
ブラウザ最適化ブラウザエンジンレベルで最適化
ネイティブ実装の性能メリット
JavaScriptエンジンレベルで処理
開発者が直接最適化必要
リソース使用量低いCPU使用率
GPUアクセラレーション活用可能
高いCPU使用率
特に高頻度イベントで負担
予測可能性実行時点予測困難
ブラウザ内部スケジューリングに依存
実行時点予測可能
イベント発生と同時に実行
デバッグ容易性非同期的特性でデバッグ複雑
コールバック実行時点追跡困難
同期的実行でデバッグ容易
イベント-コールバック関係明確
メモリ管理WeakRefパターン使用
自動ガベージコレクション対応
明示的メモリ管理必要
リスナー解除直接処理
エラー処理エラー発生時全体観察中断なし
個別entry単位でエラー分離
エラー発生時該当イベントのみ影響
他のイベント処理に影響なし
クロスブラウザ動作標準スペック準拠
ブラウザ別最適化差異存在
古い標準で一貫性高い
ブラウザ別差異最小

簡単にまとめると上記のようになります。

感覚がつかめましたか?まだ曖昧だと思うので、下記の性能特性比較を通じてさらに詳しく見ていきましょう。

性能特性比較

もう少し分かりやすくするためにコードで見てみましょうか?

JavaScript

どうですか?感覚がつかめましたか?つかめなくても大丈夫です笑。

これから本格的に一つずつ探求していきましょう。

イベントハンドラーの深掘り

まずイベントハンドラーから見ていきましょう。

イベントハンドラーの本質的な特性

イベントハンドラーを理解するためにはまずイベント駆動プログラミングの哲学を把握する必要があります。

これは何かが起きたら即座に反応するというリアクティブプログラミングの核心概念です。

まるで門番がドアをノックされたら即座にドアを開けるように、イベントハンドラーは特定のシグナル(イベント)を受け取ると予め決められた行動(コールバック)を即座に実行します。

この時、「即座に」という概念がとても重要です。ユーザーがボタンをクリックする瞬間、ブラウザはそのクリックを検知し、登録されたイベントハンドラーを見つけてすぐに実行します。

この過程で遅延や最適化のための待機時間はありません。

JavaScript

イベントループとの関係

イベントハンドラーの実行方式を理解するためには、JavaScriptイベントループ(Event Loop)を理解する必要があります。

これはシングルスレッドであるJavaScriptがどのように非同期作業を処理するかを示す核心メカニズムです。

ユーザーがクリックすると、ブラウザは即座にそのイベントをタスクキュー(Task Queue)に入れます。

イベントループは現在実行中のコードが終わると即座にタスクキューからイベントを取り出して該当ハンドラーを実行します。

この過程で優先度が高いため、他の作業より先に処理されます。

JavaScript

高い実行頻度の問題点

イベントハンドラーの「即座に実行」特性は時々性能問題を起こします。

特に高頻度イベントでこの問題が顕著になります。

スクロールやマウスの動きのようなイベントは毎秒数十回から数百回まで発生することがあり、毎回コールバックが実行されるとブラウザが負担になることがあります。

JavaScript

この問題を解決するために開発者はスロットリング(Throttling)デバウンシング(Debouncing)のような技法を使用します。

しかしこれはイベントハンドラー自体の限界を示すものでもあります。

JavaScript


同期的実行のメリット・デメリット

イベントハンドラーは同期的に実行されます。これはイベントが発生すると他のすべてのJavaScriptコードの実行を一時停止してハンドラーを先に処理するという意味です。

このような特性は諸刃の剣のようなものです。

メリットとしては予測可能性があります。ユーザーがボタンをクリックすると正確にその瞬間にハンドラーが実行されるので、ユーザーインターフェースの反応性が良くなります。

またデバッグする時もイベント発生とハンドラー実行の間の因果関係が明確に見えます。

JavaScript

しかしデメリットもあります。ハンドラーの中で長くかかる作業を行うと、ページ全体が止まったように見えることがあります。これはユーザー体験を大きく損なう要素です。

JavaScript

イベントハンドラーのこのような特性を理解すると、なぜ特定の状況ではオブザーバーがより良い選択なのか分かるようになります。オブザーバーはこのような問題をブラウザレベルで解決するために設計された別のアプローチでもあります。

同期的実行とは?

さらに詳しく入る前に「同期的実行」が何かを明確に理解しましょう。

普通、同期的実行というと、コードが1行ずつ順番に実行されることを思い浮かべます。しかし、実際には単純に「順番に実行される」という意味ではありません。

より明確に言うと、「一度に一つの作業のみ実行し、現在の作業が完全に終わるまで他のすべての作業が待つ」という意味です。

一度考えてみましょう。皆さんが本を読んでいる時、誰かが話しかけてきたと仮定してください。皆さんは読書を完全にやめてその人の話に集中し、会話が終わったら再び読書を始めるでしょう。

これがまさに同期的実行の方式です。二つのことを同時にせず、一つずつ完全に処理する方式なのです。

JavaScript

この例でユーザーがボタンをクリックすると、クリックイベントハンドラーが完全に終わるまで他のすべてのJavaScript作業が止まります。

他のクリック、タイマーコールバック、ネットワーク応答処理などすべてが待たなければならないのです。

予測可能性の真の意味

同期的実行の最大のメリットは予測可能性です。しかしこれは単に「実行順序を予測できる」という意味ではありません。

より重要なのは状態の一貫性を保証するという点です。

例えば、ユーザーが「保存」ボタンをクリックしたと考えてみてください。この瞬間に複数の作業が必要かもしれません。

フォームデータを検証し、UIを更新し、サーバーにリクエストを送る必要があるかもしれません。同期的実行ではこのような作業がアトミック(atomically)に処理されます。

JavaScript

もしこの過程が途中で切れる可能性があったらどうなるでしょうか?

ユーザーが速く複数回クリックした時、フォーム検証は完了したのにUI更新はされていない状態が発生する可能性があります。このような中間状態はバグの原因になりやすいです。

即座のフィードバックの心理的効果

同期的実行が提供する即座のフィードバックは単に技術的なメリットを超えてユーザー体験の核心です。

人間の脳は行動と結果の間の遅延をとても敏感に検知します。

研究によると、100ミリ秒以下の応答時間ではユーザーが「即座に」と感じ、1秒を超えると「遅い」と認識します。

JavaScript

このような即座のフィードバックはユーザーがインターフェースを「生きている」ものとして感じさせます。

ボタンを押すと即座に反応し、マウスを乗せるとすぐにハイライトされるものがすべて同期的実行のメリットです。

デバッグでの明確な因果関係

開発者の立場で同期的実行の最大のメリットの一つはデバッグの容易さです。

イベントが発生してハンドラーが実行される過程が明確で予測可能なため、問題が発生した時に原因を見つけやすいです。

JavaScript

これと対照的に、非同期的に処理される作業は実行順序が予測しにくく、デバッグする時にずっと複雑な状況を作り出します。

メインスレッドブロッキングの実際の影響

しかし同期的実行のデメリットも明確です。最も深刻な問題はメインスレッドブロッキングです。これを具体的に理解してみましょう。

JavaScriptはUIスレッドと同じスレッドで実行されます。つまり、JavaScriptコードが実行される間は画面レンダリング、ユーザー入力処理、アニメーションなどすべてのUI関連作業が止まります。これがまさに「ブロッキング」の意味です。

JavaScript

このような状況でユーザーは「ウェブサイトが遅い」または「バグがある」と感じるようになります。特にモバイル機器ではこのような問題がより顕著になります。

性能問題の累積効果

メインスレッドブロッキングは単にその瞬間だけの問題ではありません。

累積効果があります。ブラウザは毎秒60フレームを目標にしますが、これは16.67ミリ秒ごとに画面を再描画する必要があるという意味です。

もしイベントハンドラーがこの時間を超えて実行されると、フレームドロップが発生します。

JavaScript

このような問題は特にインタラクションが多いウェブアプリケーションで深刻なユーザー体験の低下を引き起こします。ゲーム、グラフィックエディタ、リアルタイムチャートなどではこのような性能問題が致命的になりえます。

解決戦略

同期的実行のデメリットを克服するために開発者は様々な戦略を使用します。最も基本的なのは作業分割です。

JavaScript

このような方式で同期的実行のメリットを維持しながらデメリットを最小化できます。しかしこれも根本的な解決策ではありません。

このような限界のためにIntersectionObserverのような新しいAPIが登場したのです。

オブザーバーパターンの深掘り

これでイベントハンドラーの同期的実行方式を深く理解したので、オブザーバーの世界に入ってみましょう。

前でもオブザーバーパターンが何かについて見ましたが、もう少し深く入るセッションだと思っていただければと思います。

オブザーバーを理解することはまるで全く異なる思考方式を学ぶことと同じです。イベントハンドラーが「何か起きたら即座に反応しろ」という命令型アプローチなら、オブザーバーは「何かを見守っていて変化があったら教えて」という宣言型アプローチです。

オブザーバーを理解するためにはまずその哲学的基礎を把握する必要があります。

イベントハンドラーでは開発者が能動的に「いつ何を確認するか」を決定します。

例えばスクロールイベントを聴いて、その瞬間ごとに要素の位置を確認する方式です。

しかしオブザーバーでは「何を観察するか」だけを定義し、「いつ確認するか」はブラウザに任せます。

これはまるで警備員とCCTVの違いのようです。

警備員(イベントハンドラー)は巡回しながら直接状況を確認しなければなりませんが、CCTV(オブザーバー)は設置だけしておけば自動的に監視していて問題が発生したら教えてくれます。

CCTVがより効率的な理由は24時間持続的に観察でき、複数の場所を同時に監視でき、実際に問題が発生した時だけアラームを送るからです。

JavaScript

ブラウザ最適化サイクルとの同期化

オブザーバーの最も重要な特徴の一つはブラウザのレンダリングサイクルと同期化されるという点です。

これはとても重要な概念ですが、これを理解するにはブラウザがどのように画面を描くか知る必要があります。

ブラウザは一般的に毎秒60フレームで画面を更新します。

これは約16.67ミリ秒ごとに次のような過程を経るという意味です。

まずJavaScript実行、スタイル計算、レイアウト計算、ペイント、コンポジットの順序で進行します。IntersectionObserverのコールバックはこのレンダリングパイプラインの特定の時点で実行されます。

JavaScript

このような同期化のおかげでオブザーバーは正確な情報を提供できます。

ブラウザがすでにすべての計算を終えた後にコールバックを実行するため、開発者が受け取る情報は信頼できる最新状態です。

バッチ処理の効率性

オブザーバーがイベントハンドラーより効率的な別の理由はバッチ処理方式です。

複数の要素の状態が同時に変更されても、オブザーバーはこれを一つのコールバック呼び出しでまとめて処理します。これは性能上大きなメリットを提供します。

考えてみてください。ページに100個の画像があり、ユーザーが速くスクロールしたらどうなるでしょうか?

イベントハンドラー方式ではスクロールイベントが発生するたびに100個の画像の位置をすべて確認しなければなりません。

しかしオブザーバー方式ではブラウザが「今回のフレームで変化があった画像だけ」集めて一度に教えてくれます。

JavaScript

この例で注目すべき点はユーザーがいくら速くスクロールしても、各レンダリングフレームごとに最大1回のコールバックだけ実行されるということです。

もし1フレームで10個の画像が同時に画面に表示されたなら、10回の個別呼び出しの代わりに10個の要素が入った配列と一緒に1回のコールバックが実行されます。

ノンブロッキング実行の意味

オブザーバーのもう一つの核心特徴はノンブロッキング実行です。

これは単に「メインスレッドをブロックしない」という意味を超えて、より根本的な実行哲学の違いを示しています。

イベントハンドラーではハンドラー関数が実行される間、他のすべての作業が待たなければなりません。

しかしオブザーバーではコールバック関数自体はやはりメインスレッドで実行されますが、観察作業自体はブラウザの他のシステムで処理されます。

これは観察対象がいくら多くてもJavaScript実行にはほとんど影響を与えないという意味です。

JavaScript

ブラウザエンジンレベルの最適化

オブザーバーが強力な理由の一つはブラウザエンジンレベルで最適化されているという点です。

これはJavaScriptでは実装しにくいレベルの最適化を提供します。

例えば、ブラウザはGPUを活用して変換行列計算を加速化できます。

またブラウザは実際に画面に見えない要素に対しては不必要な計算を省略できます。

このような最適化はJavaScriptレベルではアクセスできないブラウザ内部の情報を活用します。

JavaScript

メモリ管理の自動化

オブザーバーはメモリ管理の面でもイベントハンドラーと異なるアプローチをします。

オブザーバーはWeakRefパターンを内部的に使用して、観察対象要素がDOMから削除されると自動的に該当観察を整理します。

これはメモリリークを防ぐのに大きな助けになります。

JavaScript

ここまでオブザーバーの基本的な特性を見てきました。

これでイベントハンドラーとの違いがより明確に見えますか?

ブラウザレンダリングパイプラインとオブザーバー

ブラウザレンダリングパスでオブザーバーがいつ実行されるかを一度見てみましょう。

レンダリングパスでの実行時点を理解することは、オーケストラで各楽器がいつ演奏されるかを知ることと同じです。

各オブザーバーはブラウザのレンダリングパイプラインで互いに異なる時点で実行され、この時点を理解するとなぜ特定のオブザーバーが特定の作業に適しているか分かるようになります。

ブラウザレンダリングパイプラインの理解

ブラウザが1フレームをレンダリングする過程を順を追って見ていきましょう。

この過程を理解してこそ各オブザーバーがどの段階で実行されるか明確に把握できます。

ブラウザのレンダリングパイプラインは一般的に次のような順序で実行されます。

まずJavaScript実行段階でユーザーコードとイベントハンドラーが実行されます。

次にスタイル計算段階でCSSルールを適用して各要素の最終スタイルを決定します。

その後レイアウト段階で各要素の正確な位置とサイズを計算します。

ペイント段階では実際のピクセルデータを生成し、最後にコンポジット段階で複数のレイヤーを合成して最終画面を作ります。

各オブザーバーはこのパイプラインの互いに異なる地点で実行され、これは彼らがアクセスできる情報と性能特性を決定

します。

考えてみましょう。レイアウトが計算される前に実行されるオブザーバーは正確な位置情報を得ることができませんが、レイアウト後に実行されるオブザーバーは正確な情報を提供できます。

IntersectionObserver: レイアウト後に実行

IntersectionObserverは最も広く知られているオブザーバーで、レイアウト計算が完了した後に実行されます。

これはとても重要な特性で、要素の正確な位置とサイズ情報が必要だからです。

JavaScript

IntersectionObserverがレイアウト後に実行されるということは性能上大きな意味があります。

もしJavaScriptで直接getBoundingClientRect()を呼び出すと、ブラウザは即座にレイアウトを強制的に計算しなければなりません。

しかしIntersectionObserverはブラウザが自然にレイアウトを計算した後に実行されるので、不必要なレイアウト再計算を防ぎます。

ResizeObserver: レイアウト直後に実行

ResizeObserverは要素のサイズ変化を検知するオブザーバーです。

これはIntersectionObserverと似た時点で実行されますが、少し早い時点で実行される可能性があります。

ResizeObserverはレイアウト計算段階ととても密接に接続されているからです。

JavaScript

ResizeObserverの面白い特性の一つは無限ループを防ぐメカニズムがあるということです。

もしResizeObserverコールバック内で観察対象のサイズを変更したら、ブラウザはこれを検知して適切に処理します。

これは複雑なレスポンシブレイアウトを安全に実装できるようにしてくれます。

MutationObserver: DOM変更直後に実行

MutationObserverは他のオブザーバーとは実行時点が完全に異なります。

これはDOM構造の変化を検知するので、レンダリングパイプラインの最も初期の段階で実行されます。

正確にはDOM変更が起きた直後、しかしスタイル計算やレイアウト計算が開始される前に実行されます。

JavaScript

MutationObserverが初期段階で実行されるということは重要な意味があります。

これはDOM構造変更に対する即座の反応を可能にしますが、同時にまだ最終的なレイアウト情報は使用できないという意味でもあります。

したがってMutationObserverは主にDOM構造自体を管理する用途で使用されます。

PerformanceObserver: 性能測定専門

PerformanceObserverはブラウザの性能メトリクスを観察する特別なオブザーバーです。

これはレンダリングパイプラインの様々な地点で収集された性能データを提供するので、他のオブザーバーとは完全に異なる実行パターンを持っています。

JavaScript

PerformanceObserverはウェブアプリケーションの性能をリアルタイムでモニタリングするのにとても便利です。

特にCore Web Vitalsのようなユーザー体験メトリクスを測定する時に必須のツールです。

オブザーバーの実行順序と相互作用

複数のオブザーバーが同時に活性化されている時、ブラウザはどのような順序でこれらを実行するでしょうか?

これは各オブザーバーがレンダリングパイプラインのどの地点で実行されるかと直接的に関連しています。

JavaScript

この例を実行すると、オブザーバーがブラウザのレンダリングパイプライン順序に従って実行されることを確認できます。

このような実行順序を理解すると、複雑な相互作用があるアプリケーションでも予測可能な動作を実装できます。

まとめ

記事がかなり長くなりましたね。

今回のテーマを通じて、イベントハンドラーとオブザーバーの違い、そしてブラウザレンダリングパイプラインでオブザーバーがどのタイミングで実行されるかなどを詳しく知ることができれば幸いです。

気になる点はコメントで残していただければ確認次第すぐに返信しますね!

長い記事を読んでいただきありがとうございました。🙇