1. はじめに
ページの読み込み中やユーザーがページとやり取りしている間、アプリケーションやブラウザーは様々なイベントをキューに入れ、ブラウザーがそれらを実行します。例えば、ユーザーエージェントはユーザーの操作に基づいて入力イベントをスケジュールし、アプリケーションは requestAnimationFrame や他のコールバックのためにコールバックをスケジュールします。キューに入ったイベントは、ブラウザーによって一つずつ取り出されて実行されます。
しかし、いくつかのタスクは長時間(複数フレーム)かかる場合があり、その際UIスレッドがブロックされ、他のすべてのタスクもブロックされることがあります。ユーザーにとっては、これはしばしば「固まった」ページとして現れ、ブラウザーがユーザー入力に応答できなくなります。これは現在ウェブ上でのユーザー体験の大きな悪化要因です:
- 「操作可能になるまでの時間」の遅延:
-
ページの読み込み中や、完全に視覚的に表示されていても、ロングタスクがメインスレッドを占有し、ユーザーがページとやり取りするのを妨げます。設計の悪いサードパーティコンテンツが原因となることが多いです。
- 入力遅延の増加・ばらつき:
-
重要なユーザー操作イベント(例:タップ、クリック、スクロール、ホイールなど)がロングタスクの後ろに並べられるため、体験がぎこちなく予測困難になります。
- イベント処理遅延の増加・ばらつき:
-
入力イベントと同様、イベントコールバック(例:onloadイベントなど)の処理が遅れ、アプリケーションの更新が遅延します。
- アニメーションやスクロールのカクつき:
-
一部のアニメーションやスクロール操作は、コンポジタスレッドとメインスレッドの協調が必要ですが、メインスレッドがロングタスクでブロックされると、アニメーションやスクロールの応答性に影響します。
一部のアプリケーションやRUMベンダーは、すでに「ロングタスク」が発生したケースの特定や追跡を試みています。例えば、既知のパターンとして、短い周期的なタイマーを設置し、連続するタイマーの満了間の経過時間を調べる方法があります。経過時間がタイマー周期を超えていれば、ロングタスクによってイベントループの実行が遅延した可能性が高いです。この方法は一応機能しますが、パフォーマンス面で多くの問題があります:ロングタスク検出のためにポーリングすると、アイドル状態や長時間の休止が防げてしまい(requestIdleCallback参照)、バッテリー寿命に悪影響を及ぼします。また、遅延の原因(ファーストパーティかサードパーティのコードか)は分かりません。
RAILパフォーマンスモデルは、アプリケーションがユーザー入力に100ms以内で応答するべきだと提案しています(タッチ移動やスクロールの場合は16ms)。このAPIの目的は、アプリケーションがこれらの目標を達成できなくなる原因となるタスクについて通知を提供することです。本APIは50ms以上かかるタスクを検出します。これらのタスクがないウェブサイトは、ユーザー入力に100ms以内で応答できるはずです:ユーザー入力が発生したとき実行中のタスクを50ms未満で終了し、入力への反応タスクも50ms未満で実行できます。
1.1. 利用例
const observer= new PerformanceObserver( function ( list) { for ( const entryof list. getEntries()) { // ロングタスク通知の処理: // 分析や監視のための報告 // ... } }); // 過去および今後のロングタスク通知用にオブザーバーを登録 observer. observe({ type: "longtask" , buffered: true }); // この後に長いスクリプト実行があると、 // "longtask" エントリがオブザーバーにキューされ受け取れる
2. 用語
ロングタスクは、以下のいずれかの事象で50msを超えて継続するものを指します:
-
イベントループのタスクと、その直後に実行されるマイクロタスクチェックポイントの実行。これによりイベントループタスクの所要時間(関連するマイクロタスクを含む)が計測されます。
-
レンダリングの更新ステップ(イベントループ処理モデル内)。
-
最後のステップから次の最初のステップまでの間の一時停止(イベントループ処理モデル)。これにより、ユーザーエージェントがUIスレッドでイベントループ外で行う作業も含まれます。
閲覧コンテキストコンテナは、閲覧コンテキスト bcに対して、 bc の アクティブドキュメント の ノードナビゲーブル の コンテナです。
注記: この用語は古くなっており、今後改訂する際は新しい用語が再利用されるべきです。
加害閲覧コンテキストコンテナは、閲覧コンテキストコンテナ
(iframe、
object
など)が、
ロングタスクの原因として全体的に関与している場合に指します。
帰属は、ロングタスクに大きく寄与した作業の種類(スクリプト、レイアウトなど)を特定し、またその作業に責任を持つ 加害閲覧コンテキストコンテナがどれであるかを特定することを指します。
3. ロングタスクタイミング
ロングタスクタイミングには次の新しいインターフェースが含まれます:
3.1.
PerformanceLongTaskTiming
インターフェース
[Exposed =Window ]interface :PerformanceLongTaskTiming PerformanceEntry { /* Overloading PerformanceEntry */readonly attribute DOMHighResTimeStamp ;startTime readonly attribute DOMHighResTimeStamp ;duration readonly attribute DOMString ;name readonly attribute DOMString ;entryType readonly attribute FrozenArray <TaskAttributionTiming >attribution ; [Default ]object (); };toJSON
PerformanceLongTaskTiming
の属性値は、§ 4.1 ロングタスクの報告にある処理モデル内で設定されます。以下は、それらがどのように設定されるかの説明(参考)です。
name
属性のgetterは、次のいずれかの文字列を返します:
- "
unknown" -
ロングタスクが、ユーザーエージェントがイベントループ外で行った作業から発生した場合。
- "
self" -
ロングタスクが、このイベントループタスク内で、現在のブラウジングコンテキストで発生した場合。
- "
same"- origin- ancestor -
ロングタスクが、イベントループタスク内で、同一オリジンの祖先ナビゲーブルで発生した場合。
- "
same"- origin- descendant -
ロングタスクが、イベントループタスク内で、同一オリジンの子孫ブラウジングコンテキストで発生した場合。
- "
same"- origin -
ロングタスクが、イベントループタスク内で、祖先でも子孫でもない同一オリジンのブラウジングコンテキストで発生した場合。
- "
cross"- origin- ancestor -
ロングタスクが、イベントループタスク内で、クロスオリジンの祖先ナビゲーブルで発生した場合。
- "
cross"- origin- descendant -
ロングタスクが、イベントループタスク内で、クロスオリジンの子孫ブラウジングコンテキストで発生した場合。
- "
cross"- origin- unreachable -
ロングタスクが、イベントループタスク内で、祖先でも子孫でもないクロスオリジンのブラウジングコンテキストで発生した場合。
- "
multiple"- contexts -
ロングタスクが、複数のブラウジングコンテキストをまたぐイベントループタスクから発生した場合。
注: これらの名前には「-unreachable」や「-contexts」など、いくつか一貫性がないものがあります。 互換性維持のため、これらの名前はそのままとされています。
entryType
属性のgetterは、 を返します。
startTime
属性のgetterは、タスクの開始時刻のDOMHighResTimeStamp
を返します。
duration
属性のgetterは、タスク開始から終了までの経過時間を、1msの粒度で表したDOMHighResTimeStamp
を返します。
attribution 属性のgetterは、TaskAttributionTiming
エントリの凍結された配列を返します。
3.2.
TaskAttributionTiming
インターフェース
[Exposed =Window ]interface :TaskAttributionTiming PerformanceEntry { /* Overloading PerformanceEntry */readonly attribute DOMHighResTimeStamp ;startTime readonly attribute DOMHighResTimeStamp ;duration readonly attribute DOMString ;name readonly attribute DOMString ;entryType readonly attribute DOMString containerType ;readonly attribute DOMString containerSrc ;readonly attribute DOMString containerId ;readonly attribute DOMString containerName ; [Default ]object (); };toJSON
TaskAttributionTiming
の属性値は、§ 4.1 ロングタスクの報告にある処理モデル内で設定されます。以下は、それらがどのように設定されるかの説明(参考)です。
name
属性のgetterは常に"unknown"を返します。
entryType
属性のgetterは常に"taskattribution"を返します。
startTime
属性のgetterは常に0を返します。
duration
属性のgetterは常に0を返します。
containerType
属性のgetterは、原因となるブラウジングコンテナの種類("iframe"、"embed"、"object"など)を返します。単一の原因となるブラウジングコンテナが見つからない場合は、"window"を返します。
containerName
属性のgetterは、コンテナの name
コンテンツ属性の値を返します。単一の原因となるブラウジングコンテナが見つからない場合は、空文字列を返します。
containerId
属性のgetterは、コンテナの id
コンテンツ属性の値を返します。単一の原因となるブラウジングコンテナが見つからない場合は、空文字列を返します。
containerSrc
属性のgetterは、コンテナの src
コンテンツ属性の値を返します。単一の原因となるブラウジングコンテナが見つからない場合は、空文字列を返します。
3.3. 原因の特定
このセクションは規定ではありません。
ロングタスクは、スクリプト・レイアウト・スタイルなど様々な種類の作業を含む場合があり、異なるブラウジングコンテキスト内で実行されたり、全体的(ガベージコレクションなど)なものもあり、 エージェントクラスタやブラウジングコンテキストグループセット全体にまたがる場合もあります。
このため、アトリビューションにはいくつかの側面があります:
-
ロングタスクの発生元や原因となるブラウジングコンテナの全体的な場所を指し示す:これは最小限の原因特定と呼ばれ、
nameフィールドに記録されます。 -
ロングタスクに関与した作業の種類や、その作業に関連する原因となるブラウジングコンテナの特定:これは
TaskAttributionTimingオブジェクトとして、attributionフィールドに記録されます(PerformanceLongTaskTiming)。
したがって、name
フィールドと attribution
フィールド(PerformanceLongTaskTiming)で、ロングタスクの責任箇所を示します。
この情報を提供する際はWebの同一オリジンポリシーを遵守する必要があります。
これらのフィールドは独立していません。以下はそれらの関係の概要です:
name
| 原因となるブラウジングコンテナ
(attributionで示される)
|
|---|---|
"self"
| 空 |
"same"
| 同一オリジンの原因 |
"same"
| 同一オリジンの原因 |
"same"
| 同一オリジンの原因 |
"cross"
| 空 |
"cross"
| 空 |
"cross"
| 空 |
"multiple"
| 空 |
"unknown"
| 空 |
4. 処理モデル
注: Long Tasks API を実装するユーザーエージェントは、 を supportedEntryTypes
に Window
コンテキスト用として含める必要があります。
これにより、開発者はロングタスクのサポートを検出できます。
4.1. ロングタスクの報告
-
タスク終了時刻を記録する(end time と task の document を用いる)。
-
end time から start time を引いた値がロングタスクの閾値(50ms)未満なら、この手順を中止する。
-
destinationRealms を空の集合とする。
-
レポートを配信する JavaScript Realm の集合を決定する:
各 トップレベルブラウジングコンテキスト topmostBC(top-level browsing contexts の要素)について:
-
topmostBC の アクティブドキュメント の 関連Realm を destinationRealms に加える。
-
descendantBCs を topmostBC の アクティブドキュメント の 子孫ブラウジングコンテキストのリスト とする。
-
document を descendantBC の アクティブドキュメント とする。
-
各 descendantBC(descendantBCs の要素)について、(document の 関連Realm, document の 関連設定オブジェクト の クロスオリジン分離機能) を destinationRealms に加える。
-
-
ユーザーエージェントは、JavaScript Realm の一部を destinationRealms から除外できる。
注: この除外は、ユーザーエージェントが別プロセスで扱う JavaScript Realm のロングタスク報告を回避するために利用される可能性がある。しかしこの概念は厳密には規定されていない。
どの Document
がどのロングタスクに可視性を持つかについて、議論が続いているため、このロジックは将来的に変更される可能性がある。 [Issue #75]
-
各 (destinationRealm, crossOriginIsolatedCapability) について、 destinationRealms の中から繰り返す:
-
name を空文字列に設定する。これは下記の最小加害帰属の報告で用いる。
-
culpritSettings を
に設定する。null -
task の スクリプト評価環境設定オブジェクトのセットを処理し、以下のようにして name と culpritSettings を決定する:
-
もし task の スクリプト評価環境設定オブジェクトのセット が空ならば、 name に "
unknown" を設定し、 culpritSettings にを設定する。null -
そうでなく、そのセットの長さが1より大きい場合: name に "
multiple" を設定し、 culpritSettings に- contextsを設定する。null -
それ以外、すなわちセットの長さが1の場合:
-
culpritSettings を task の スクリプト評価環境設定オブジェクトのセット の唯一の要素に設定する。
-
destinationSettings を destinationRealm の 関連設定オブジェクト とする。
-
destinationOrigin を destinationSettings の オリジンとする。
-
destinationBC を destinationSettings の グローバルオブジェクト の 閲覧コンテキストとする。
-
culpritBC を culpritSettings の グローバルオブジェクト の 閲覧コンテキストとする。
-
Assert: culpritBC は
ではない。null -
もし culpritSettings が destinationSettings と同じなら name に "
self" を設定する。 -
それ以外で、culpritSettings の オリジン と destinationOrigin が 同一オリジンの場合:
-
もし destinationBC が
ならば name に "null same" を設定する。- origin -
そうでなく、culpritBC が destinationBC の 祖先の場合、 name に "
same" を設定する。- origin- ancestor -
そうでなく、destinationBC が culpritBC の 祖先の場合、 name に "
same" を設定する。- origin- descendant -
それ以外は name に "
same" を設定する。- origin
-
-
それ以外:
-
もし destinationBC が
ならば name に "null cross" を設定する。- origin- unreachable -
そうでなく、culpritBC が destinationBC の 祖先の場合、 name に "
cross" を設定し、culpritSettings に- origin- ancestorを設定する。null 注記: セキュリティ上の理由からこれは報告されません。開発者は自身で調査する必要があります。
-
そうでなく、destinationBC が culpritBC の 祖先の場合、 name に "
cross" を設定する。- origin- descendant -
それ以外は、name に "
cross" を設定する。- origin- unreachable
-
-
-
-
attribution を新たな
TaskAttributionTimingオブジェクトとして destinationRealm で作成し、次のように属性を設定する:-
attribution の
name属性に "unknown" を設定する。注記: 今後のAPIバージョンでは
name属性に値が追加される予定だが、現時点では1つの値のみ。 -
attribution の
entryType属性にを設定する。"taskattribution" -
attribution の
containerType属性にを設定する。"window" -
attribution の
containerNameおよびcontainerSrc属性を空文字列に設定する。 -
もし culpritSettings が
でなければ:null -
culpritBC を culpritSettings の グローバルオブジェクト の 閲覧コンテキスト とする。
-
Assert: culpritBC は
ではない。null -
container を culpritBC の 閲覧コンテキストコンテナ とする。
-
Assert: container は
ではない。null -
attribution の
containerId属性に container の ID の値、またはIDが未設定なら空文字列を設定する。 -
もし container が
iframe要素の場合:-
attribution の
containerType属性に "iframe" を設定する。 -
attribution の
containerName属性に container のnameコンテンツ属性の値、なければ空文字列を設定する。 -
attribution の
containerSrc属性に container のsrcコンテンツ属性の値、なければ空文字列を設定する。
注記: ここでは frame の
src属性のみを記録することが意図されています。実際のURLを参照しないのは、主としてフレーム識別を助ける目的であり、クロスオリジンiframeの現在URLを知ることはセキュリティ上問題となるためです。 -
-
もし container が
frame要素の場合:-
attribution の
containerType属性に "frame" を設定する。 -
attribution の
containerName属性に container のnameコンテンツ属性の値、なければ空文字列を設定する。 -
attribution の
containerSrc属性に container のsrcコンテンツ属性の値、なければ空文字列を設定する。
-
-
もし container が
object要素の場合:-
attribution の
containerType属性に "object" を設定する。 -
attribution の
containerName属性に container の name コンテンツ属性の値、なければ空文字列を設定する。 -
attribution の
containerSrc属性に container のdataコンテンツ属性の値、なければ空文字列を設定する。
-
-
もし container が
embed要素の場合:-
attribution の
containerType属性に "embed" を設定する。 -
attribution の
containerName属性に空文字列を設定する。 -
attribution の
containerSrc属性に container のsrcコンテンツ属性の値、なければ空文字列を設定する。
-
-
-
-
新たな
PerformanceLongTaskTimingオブジェクト newEntry を destinationRealm で作成し、次のように属性を設定する:-
newEntry の
name属性に name を設定する。 -
newEntry の
entryType属性に "longtask" を設定する。 -
newEntry の
startTime属性に coarsening した start time (crossOriginIsolatedCapabilityを指定)を設定する。 -
dur を coarsening した end time(crossOriginIsolatedCapabilityを指定)から newEntry の
startTimeを引いた値とする。 -
newEntry の
duration属性に dur の整数部分を設定する。 -
もし attribution が
でなければ newEntry のnull attribution属性に attribution を唯一要素とする新たな frozen 配列を設定する。注記: 今後のAPIバージョンでは
attribution属性の値が追加される予定だが、現時点では配列は1要素のみ。
-
-
PerformanceEntryをキューに追加 newEntry を行う。
-
5. セキュリティおよびプライバシーに関する考慮事項
Long Tasks APIは、ロングタスクの発生元についてオリジンが安全なアトリビューション情報を含めることで、同一オリジンポリシーに従っています。ロングタスクの閾値は50msです。継続時間は1msの粒度でのみ提供されます。これらを組み合わせることで、クロスオリジン漏洩に対して十分な保護を提供しています。
Long Tasks APIは、ユーザーによって実行されたタスクの継続時間や種類のタイミング情報、および関数呼び出しを引き起こしたブラウジングコンテキストなどのアトリビューションを提供します。これにより、攻撃者がユーザーの行動を推測したり特定したりするためのサイドチャネルタイミング攻撃を行える可能性があります。たとえば、長いスクリプトの後に長いレンダーが続くパターンから、ユーザーのソーシャルウィジェットへの操作を推測できます。詳細な関数呼び出しのアトリビューションは、ユーザーの行動を特定するために利用されます。
このAPIが新たなプライバシー攻撃を導入することはありませんが、既存のプライバシー攻撃をより迅速にする可能性があります。これに対する緩和策は必要に応じて実装できます:
-
APIが提供するロングタスクの継続時間にさらにクランプ(丸め)やランダムなジッターを加え、攻撃の実行を困難にする。
-
APIによって公開するlongtaskのオリジン数を制限し、その後のタスクのアトリビューションを曖昧化する。例えば、5つのiframeを持つページは、そのうち3つのiframeからのタスクのみアトリビューションを受け取り、他の2つについてはアトリビューションなし(
nameがunknown)となる。 -
一定の閾値を超えた後、原因/アトリビューション情報を削除できるようにする。例えば、10回のロングタスクの後は、すべてのエントリーがアトリビューションなしとなり、
nameが"unknown"となる。 -
APIによって公開されるタイミング情報に内蔵の遅延を追加し、ロングタスクの件数依存の攻撃の実行を困難にする。
5.1. 観測者に公開される内容
トップレベルページ内のすべての観測者(すなわちページ内のすべてのiframeとメインフレーム)は、ロングタスクの存在について通知を受け取ります。タスクの開始時刻、継続時間(1ms粒度)、および原因となるフレームへのポインタを公開します。これらの情報は、今日でもsetTimeoutを使えばより高い精度で観測可能です。攻撃者は、ページ上の他の要素をすべて除去し、脆弱なクロスオリジンリソースのみを追加することで、setTimeoutの遅延がそのリソースによるものであると確認できます。他ページ(タブやウィンドウ)の観測者は、ユーザーエージェントのアーキテクチャに関わらず、通知を受け取るべきではありません。
クロスオリジンで公開される内容のルール:
-
クロスオリジンの観測者は、原因の方向のみ確認できます。たとえば、原因が深くネストされたiframeなら、ホストページは自分と原因との間の最初のクロスオリジンだけを見ることができます。
-
逆に、原因がトップレベルページの場合、深く埋め込まれたiframeはクロスオリジン祖先でロングタスクが発生したことは分かりますが、それ以上の情報は受け取りません。
5.2. 考慮された攻撃シナリオ
考慮されたタイミング攻撃は以下の通りです:
-
従来のタイミング攻撃:外部リソースのロード時間を利用して、プライベートデータのサイズを明らかにする。例えば、ギャラリー内の隠し画像の数やユーザー名の有効性など。例参照。
-
サイドチャネルタイミング攻撃:動画解析、スクリプト解析、App Cacheの読み取りやCache API(Service Worker)利用の時間を使って、ユーザーの特定や年齢・性別・地域・興味などのプロファイルを作成する。例として、ソーシャルネットワークのステータス更新が特定の属性(例:20~30歳の女性)に限定される場合、パーマリンクページのファイルサイズによってユーザーがその属性かどうかが判別できる。
これらのシナリオは、50msの閾値とクロスオリジン境界の遵守、すなわちタスクタイプや追加のアトリビューションを信頼できないクロスオリジン観測者に公開しないことで対応しています。