ブラウザにはネイティブの「タイム スライシング」機能もあります。

こんにちは、私の名前はConardLiです。

このバージョンではChrome 115、ブラウザが のscheduler.yieldグレースケール テストを開始しました。は、メインスレッドに制御を戻すためのより簡単かつ優れた方法を提供する新機能scheduler.yieldです。scheduler APIこれについて説明する前にAPI、新しいパフォーマンス指標を見てみましょう。

次の描画インタラクション (INP)

次の描画インタラクション (INP) は新しい指標であり、ブラウザーは 2024 年 3 月にこれを最初の入力遅延 (FID) に置き換え、最新のものになる予定です (Web コア パフォーマンス指標については、私の記事を参照してください: 次世代Web Core Vitals解釈Web パフォーマンス エクスペリエンスと品質指標)。

Chrome使用状況データは、ユーザーがページに費やす時間の一部はページが90%読み込まれた後に費やされることを示しているため、ページのライフサイクル全体の応答性を注意深く測定することが重要であり、それがメトリクスでINP評価されます。

優れた応答性とは、ページが迅速に応答し、ユーザーと対話できることを意味します。ページがインタラクションに応答すると、最も直接的な結果は視覚的なフィードバックであり、ブラウザーによってレンダリングされる次のフレームに反映されます。たとえば、視覚的なフィードバックにより、商品が実際にカートに追加されたかどうか、Kuaidu がナビゲーション メニューを開いたかどうか、サーバーがログイン フォームの内容を認証しているかどうかなどがわかります。INPの目標は、すべてまたはほとんどのユーザー インタラクションにおいて、ユーザーがインタラクションを開始してから次のフレームが描画されるまでの時間を可能な限り短くすることです。

INPユーザーがページにアクセスする全期間を通じて発生するすべてのクリック、キーストローク、およびキーボード操作の待ち時間を観察することによって、ユーザー操作に対するページの全体的な応答性を評価する指標。

bc242b1a172e58649a2c79ecad5cd4fa.png

インタラクションは、同じ論理的なユーザー ジェスチャ中に起動される一連のイベント ハンドラーです。たとえば、タッチスクリーン デバイス上の「タップ」インタラクションには、 や などの複数のイベントが含まれpointerup、pointerdownますclickJavaScript、CSSインタラクションは、組み込みのブラウザ コントロール、またはそれらの組み合わせによって駆動できます。

d77629519e585b5a30e5cd29a2c8fca2.png

インタラクションのレイテンシーは、ユーザーがインタラクションを開始してから視覚的フィードバックの次のフレームがレンダリングされるまで、インタラクションを駆動する一連のイベント ハンドラーの単一の最長期間で構成されます。

INP はすべてのページ インタラクションを考慮しますが、最初の入力遅延 ( FID) は最初のインタラクションのみを考慮します。また、最初のインタラクションの入力レイテンシーのみが測定され、イベント ハンドラーの実行にかかる時間や次のフレームのレンダリングにかかる​​レイテンシーは測定されません。

INPブラウザーが「置換」を使用したいFIDということは、ユーザーのインタラクティブなエクスペリエンスがますます重要になっていることを意味します。私たちがよく聞くタイムスライスの概念は、実際には、Web ページのインタラクティブな応答性を向上させるためのものです。

タイムスライス

JavaScriptrun-to-completionモデルを使用してタスクを処理します。これは、タスクがメインスレッドで実行されている場合、タスクは完了するまでに必要なだけ実行されることを意味します。タスクが完了すると、メイン スレッドがキュー内の次のタスクを処理できるように、制御がメイン スレッドに戻されます。

タスクが決して完了しない特殊なケース (無限ループなど) を除いて、タスクJavaScriptのスケジューリング ロジックでは、譲歩は避けられない側面です。降伏は遅かれ早かれ起こるでしょう、それは時間の問題であり、早ければ早いほど良いのです。50タスクの実行に時間がかかりすぎる (正確にはミリ秒以上) 場合、それらは長いタスクとみなされます。

長いタスクは、ユーザー入力に対するブラウザーの応答能力を遅らせるため、ページの応答性を低下させる原因となります。長いタスクが発生し、実行に時間がかかるほど、ユーザーはページの実行が遅い、または完全にハングしていると感じる可能性が高くなります。

ただし、コードがブラウザーでタスクを開始したからといって、メイン スレッドに制御を返す前にタスクが完了するまで待機する必要があるわけではありません。タスク中に制御を明示的に放棄することで、ページ上のユーザー入力に対する応答性を向上させることができ、次の適切なタイミングでタスクを完了できるようになります。このようにして、他のタスクは、長いタスクが完了するのを待つことなく、メインスレッドでより迅速に時間を取得できます。

9e9d2da39ba9a4031ff0b9f6b6619a09.png

この図は非常に直観的に示すことができます。上記の実行では、コントロールはタスクの完了後にのみ返されます。つまり、コントロールがメインスレッドに戻るまでにタスクが完了するまでに時間がかかる可能性があります。以下では、制御の引き継ぎがまだアクティブで、長いタスクを複数の小さなタスクに分割しています。これにより、ユーザーの操作がより速く実行され、入力の応答性と速度が向上しますINP

明示的に譲歩したいときは、ブラウザーに「これから行う作業には時間がかかるかもしれないことはわかっていますが、ユーザー入力に応答する前にこれらすべてを実行する必要はありません」と伝えます。またはその他の重要なタスクである可能性があります。」

これには聞き覚えがあるでしょうか?これは実際に私たちがよく言う「タイム スライシング」の概念です。この機能を提案した最初のフロントエンド フレームワークであるため、以前に聞いたときはまだそのReact概念を覚えているかもしれません。次の典型的な例を見てみましょう。

旧バージョンのアーキテクチャは同期的に再帰更新される ノード数が多い場合、変更がReact1つであっても複雑な再帰更新が必要になる 一度更新を開始すると途中で中断できず、更新が完了するまでメインスレッドを解放できないツリー全体が走査されます。stateReact

93b269a0595b297e99ae38789a7b01cf.png

レンダリング レベルが非常に深い場合、再帰的な更新時間が 16 ミリ秒を超えます。この時点でユーザー操作やアニメーションのレンダリングがあると、フリーズとして表示されます。

8020f22018d6cd72f65e73a99246a388.gif

その後、時間のかかる更新タスクを小さな部分に分割できるReact独自の を実装しました。Schedulerこのようにして、ブラウザーはスタイル レイアウトとスタイル描画を実行するための残りの時間を確保し、フレーム ドロップの可能性を減らします。

d7bed38bfc388254f788cdd32df05c6f.png

小さなタスクがそれぞれ完了すると、制御がメイン スレッドに返され、ブラウザはユーザーの操作やページの描画を適時に完了する時間ができるため、ページは非常にスムーズになります。

a56d4d4ae24819522491871dda761abc.gif

このアイデアは素晴らしいですが、ネイティブコードや他のフレームワークJavaScriptでこの機能が必要な場合はどうすればよいでしょうか?

setTimeout を使用する

一般的な遷移方法は、時刻 0 を使用することですsetTimeoutこのアプローチが機能するのは、 に渡されたコールバックがsetTimeout残りの作業を別のタスクにオフロードし、その後の実行のためにキューに入れられるためです。これにより、大きな作業の塊をより小さな部分に分割することもできます。

ただし、setTimeoutyield を使用すると、望ましくない副作用が生じる可能性があります。yield 後の作業はタスク キューの最後に移動することになります。ユーザーの操作によってスケジュールされたタスクはタスク キューの先頭に残りますが、実行したい残りの作業は、その前にキューにある他のタスクによってさらに遅れる可能性があります。

以下に例を示します。

function blockingTask (ms = 200) {
  let arr = [];
  const blockingStart = performance.now();

  console.log(`Synthetic task running for ${ms} ms`);

  while (performance.now() < (blockingStart + ms)) {
    arr.push(Math.random() * performance.now / blockingStart / ms);
  }
}

function yieldToMain () {
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

async function runTaskQueueSetTimeout () {
  if (typeof intervalId === "undefined") {
    alert("Click the button to run blocking tasks periodically first.");
    
    return;
  }
  
  clearTaskLog();

  for (const item of [1, 2, 3, 4, 5]) {
    blockingTask();
    logTask(`Processing loop item ${item}`);
    
    await yieldToMain();
  }
}

document.getElementById("setinterval").addEventListener("click", ({ target }) => {
  clearTaskLog();

  intervalId = setInterval(() => {
    if (taskOutputLines < MAX_TASK_OUTPUT_LINES) {
      blockingTask();
    
      logTask("Ran blocking task via setInterval");
    }
  });
  
  target.setAttribute("disabled", true);
}, {
  once: true
});

document.getElementById("settimeout").addEventListener("click", () => {
  runTaskQueueSetTimeout();
});

最初にいくつかのタスクを定期的に実行するために使用しますsetinterval。次に、タイム スライシングをシミュレートし、長いタスクを逆アセンブルするために使用しますsetTimeout。次のような出力結果が得られます。

Processing loop item 1
Processing loop item 2
Ran blocking task via setInterval
Processing loop item 3
Ran blocking task via setInterval
Processing loop item 4
Ran blocking task via setInterval
Processing loop item 5
Ran blocking task via setInterval
Ran blocking task via setInterval

多くのスクリプト (特にサードパーティのスクリプト) は、特定の時間間隔で作業を実行するタイマー関数を登録することがよくあります。setTimeout長いタスクを破棄するために使用するということは、他のタスク ソースからの作業が、イベント ループの終了後に実行する必要がある残りの作業よりも先にキューに入れられる可能性があることを意味します

これは役立つかもしれませんが、多くの場合、開発者がメイン スレッドの制御を簡単に放棄することを躊躇するのは、この動作が原因です。自発的に制御を放棄できることは、ユーザーとの対話がより高速に実行される可能性があるため良いことですが、他の非ユーザー対話作業がメインスレッドで時間を稼ぐこともできます。これは確かに問題です。scheduler.yieldこの問題の解決に協力してください。

スケジューラー.収量

メインスレッドの制御を引き渡すことは のsetTimeout設計目標ではないことに注意する必要があります。その中心的な目標は、将来の特定の時点で特定のタスクを完了できるようにすることです。そのため、タスク内の作業はキューに入れられます。列の最後尾。

ただし、対照的に、デフォルトでは、scheduler.yield残りの作業はキューの先頭に送信されます。yieldこれは、直後に再開したい作業が、他のソース (ユーザー インタラクション以外) からのタスクに取って代わられることがないことを意味します。

scheduler.yieldは、呼び出されると自発的にメインスレッドに譲り、リターンする関数ですPromiseこれは、非同期関数で待機できることを意味します。

async function yieldy () {
  // Do some work...
  // ...

  // Yield!
  await scheduler.yield();

  // Do some more work...
  // ...
}

前の例を引き続き使用しますが、今回はscheduler.yieldwait を使用します。

async function runTaskQueueSchedulerDotYield () {
  if (typeof intervalId === "undefined") {
    alert("Click the button to run blocking tasks periodically first.");
    
    return;
  }

  if ("scheduler" in window && "yield" in scheduler) {
    clearTaskLog();

    for (const item of [1, 2, 3, 4, 5]) {
      blockingTask();
      logTask(`Processing loop item ${item}`);

      await scheduler.yield();
    }
  } else {
    alert("scheduler.yield isn't available in this browser :(");
  }
}

印刷結果は次のようになることがわかります。

Processing loop item 1
Processing loop item 2
Processing loop item 3
Processing loop item 4
Processing loop item 5
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval

このようにして、両方の長所を実現できます。長いタスクを分割し、積極的にメインスレッドに制御を放棄して、Web サイトのインタラクティブな応答速度を向上させるだけでなく、タスクを与えた後に完了する作業を確実に行うことができます。メインスレッドの起動は遅延しません。

試してみる

Scheduler.yield興味があり、試してみたい場合は、Chrome 115次のバージョンから始めてください。

これをオンにしてchrome://flags、[有効にする] を選択するとExperimental Web Platform Features、 を使用できるようになりますScheduler.yield公式 Polifill を使用してみることもできます: https://github.com/GoogleChromeLabs/scheduler-polyfill

setTimeoutビジネス コードで使用する場合、サポートされていない低バージョンのブラウザーとの互換性を保つために、サポートされていない場合はその記述方法にフォールバックできます。

// A function for shimming scheduler.yield and setTimeout:
function yieldToMain () {
  // Use scheduler.yield if it exists:
  if ('scheduler' in window && 'yield' in scheduler) {
    return scheduler.yield();
  }

  // Fall back to setTimeout:
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

// Example usage:
async function doWork () {
  // Do some work:
  // ...

  await yieldToMain();

  // Do some other work:
  // ...
}

もちろん、自分のタスクが他のタスクによって遅れることを望まない場合は、この API がサポートされていない場合に譲歩しないことを選択することもできます。

// A function for shimming scheduler.yield with no fallback:
function yieldToMain () {
  // Use scheduler.yield if it exists:
  if ('scheduler' in window && 'yield' in scheduler) {
    return scheduler.yield();
  }

  // Fall back to nothing:
  return;
}

// Example usage:
async function doWork () {
  // Do some work:
  // ...

  await yieldToMain();

  // Do some other work:
  // ...
}

やっと

この API についてどう思いますか? コメント欄にメッセージを残してください。

参考:https://developer.chrome.com/en/blog/introducing-scheduler-yield-origin-trial/

- 終わり -

Qi Wu 劇団について

Qi Wu Troupe は 360 グループ最大のフロントエンド チームであり、グループを代表して W3C および ECMA メンバー (TC39) の活動に参加しています。Qi Wu Troupe は人材育成を非常に重視しており、エンジニア、講師、翻訳者、ビジネス インターフェース担当者、チーム リーダー、その他の開発方向性を従業員が選択できるほか、技術スキル、専門スキル、一般スキル、リーダーシップスキルなどのコース。Qi Dance Troupe は、あらゆる種類の優れた才能に注目し、オープンで才能を求める姿勢で Qi Dance Troupe に参加することを歓迎します。

6d9134f218714f83b1b7aeca71d4a591.png

おすすめ

転載: blog.csdn.net/qiwoo_weekly/article/details/132594468