カランのブログ

ソフトウェアエンジニア / 台湾人 / 福岡生活

今のモード ライト

多くのウェブページにはさまざまな script の実行が必要ですが、もちろん優先度もあります。例えば、重要なタスクとしては、UIのレンダリング、関連するインタラクティブなイベントの登録、データのAPI呼び出しなどがあります。一方、重要でないタスクとしては、アナリティクスのスクリプト、遅延読み込み、重要でないイベントの初期化などがあります。

アイドル状態とは?

ブラウザがアイドル状態にあるかどうかを知るにはどうすればいいでしょうか?これは非常に複雑な問題です。ブラウザは私たちのために一連のタスクをスケジュールしており、HTML、CSS、JavaScriptの解析、UIのレンダリング、APIの呼び出し、画像の取得と解析、GPUのアクセラレーションなどを行っています。ブラウザのスケジューリング作業を理解するためには、いつアイドル状態になるかを知る必要があります。しかし、幸いなことに、requestIdleCallback がこの問題を解決してくれます。

requestIdleCallbackの概要

requestIdleCallback はフレームの最後に実行されますが、すべてのフレームで requestIdleCallback が実行されることは保証されません。その理由は非常に単純です。すべてのフレームの終了時にまだ時間があるとは限らないため、requestIdleCallback の実行時間を保証することはできません。

よく見ると、requestIdleCallback はコンテキストスイッチのような感じがします。フレームとフレームの間で作業を完了し、一時停止してから再開します。

requestIdleCallback(fn, {timeout})

const myWork = deadline => {
  console.log("not important job.")
  while (deadline.timeRemaining() > 0) {
    // sending logging
    // fetch non-essential data.
    // use your imaginary
  }
  abortMyJob()
}

また、2番目の引数のオプションには timeout オプションがあります。ブラウザが指定された時間内に呼び出さない場合、このタイムアウトを使用してブラウザを強制的に停止することができます。

これは適切な使用方法ではありません。アイドルの理由は、このタスクが重要ではないため、手元の作業を中断する必要はありません。ブラウザはこの柔軟性を提供してくれますが、時にはイベントが特定のタイミングでトリガされることを望むかもしれません。

cancelIdleCallback(id)

requestIdleCallback() に対応するidが返されますので、不要なアイドルコールバックをキャンセルするために cancelIdleCallback(id) を呼び出すこともできます。

requestIdleCallbackは中断される可能性がありますか?

deadline パラメータは、このフレームでタスクを完了するために利用できる時間を示しています。渡されたコールバックはこのパラメータを取得し、公式の説明によれば、この時間を超えてもブラウザはタスクを強制的に中断しません。ただし、deadline を完了させてユーザーに最適な体験を提供することを期待しています。

deadline.timeRemaining() は、現在利用可能な時間を返します。

requestIdleCallback内でDOM操作を行うとどうなりますか?

考えてみましょう。先に述べたように、requestIdleCallback はフレームの最後に実行されるため、ブラウザは既に再計算、レイアウト、ペイントの作業を完了しています。その時点でDOMを変更すると、ブラウザに再計算、レイアウト、ペイントのスケジュールを再度行わせることになります。

requestIdleCallback内でrequestIdleCallbackを呼び出すとどうなりますか?

requestIdleCallback 内で requestIdleCallback を呼び出すことは有効です。ただし、このコールバックは次のフレームにスケジュールされます。(実際には必ずしも次のフレームではなく、ブラウザのスケジューリングによります)

例を見せてください!

仮に、いくつかのユーザーがいるとしましょう。マウスをアバターの上に移動すると、個人の紹介が表示されます。アイドルの期間を活用するために、requestIdleCallback 内で必要なAPIを先に取得し、まだデータをフェッチしていない場合にのみAPIを呼び出してデータを取得します。

function fetchUser(name) {
  const users = {
    kalan: "food, coffee, life",
    jack: "woman, coffee, life",
  }
  return Promise.resolve(users[name])
}

const userIntro = {}

const queue = [
  { name: "kalan", fetched: false },
  { name: "jack", fetched: false },
]

requestIdleCallback(deadline => {
  while (deadline.timeRemaining() > 0) {
    let q = queue.pop()
    fetchUser(q.name).then(user => {
      if (deadline.timeRemaining() > 0) {
        userIntro[user.name] = user
        q.fetched = true
      }
    })
  }
}, 500)

avatar.addEventListener("mouseover", e => {
  const name = e.target.getAttribute("data-name")
  if (userIntro[name]) {
    // show intro
  } else {
    fetchUser(name).then(user => showInfo(user))
  }
})

この例では、requestIdleCallback を使用してユーザーのデータを取得して保存します。その後、ユーザーがアバターをクリックした場合、直接表示することができます。データが取得できていない場合は、再度 fetchUser を呼び出します。

requestIdleCallback の効果を示すために、この例は少し面倒です。キューを管理して、すでに fetch されたかどうかを判断する必要があります。さらに、mouseover がトリガーされた場合、userIntro の値を別途チェックする必要があります。開発の観点からは、複数のユーザーのデータを一度に取得する方が簡単ですが、これは要件と使用シナリオによるものです。

アナリティクス

もう1つの一般的なシナリオは、トラッキングです。たとえば、ユーザーがボタンをクリックした、再生ボタンをクリックした、視聴時間がどれくらいかなどを追跡することがあります。イベントを収集し、requestIdleCallback を使用して一度に送信することができます。例えば:

const btns = btns.forEach(btn => // buttons you want to track.
btn.addEventListener('click', e => {
    // do other interactions...
    //...
    putIntoQueue({
      type: 'click'
      // collect your data
    }));
    schedule();
});

function schedule() {
    requestIdleCallback(
      deadline => {
          while (deadline > 0) {
            const event = queues.pop();
            send(event);
          }
      },
      { timeout: 1000 }
   );
}

ここでは、スケジュールを呼び出すために timeout を追加しています。

Reactではどうですか?

他の不要な作業がメインスレッドを妨害しないようにするために、Reactのフックを統合して、すべての不要な作業を useIdleCallback に入れることができます。

import { useEffect, useRef } from "react"

function useIdleCallback(callback, timeout) {
  useEffect(() => {
    let id = requestIdleCallback(deadline => {
      callback(deadline)
    }, timeout)
    return () => cancelIdleCallback(id)
  }, [callback, timeout])
}

function UserIntro({ data }) {
  useIdleCallback(() => {
    sendLog(data)
  })
  return // your awesome UI
}

いくつかの問題

これらの例は便宜上示したものであり、実際にはさまざまな問題があります。キューの管理方法、timeout の管理方法、タスクの優先度をマークする方法など、最適化できる箇所はたくさんあります。

結論

ブラウザのサポート度はまだ十分ではありません:

螢幕快照 2019-03-15 上午10.42.06

ReactのFiberでも、実際には似たようなメカニズムを使用しています。過去には、作業を実行する際にスタックの深さが深すぎたり、updateQueue が多すぎたりすると、メインスレッドがブロックされ、更新が長時間かかることがありました。Fiberのメカニズムは、作業を小さな塊に分割し、他の優先度の高い作業を先に処理し、その後で他の作業を処理するようにします。

次の記事

デバッグプロセスを記憶する — 接続

前の記事

Goworker 入門 — Redis によるワーカーの実装

この文章が役に立つと思うなら、下のリンクで応援してくれると大変嬉しいです✨

Buy me a coffee

作者

Kalan 頭像照片,在淡水拍攝,淺藍背景

愷開 | Kalan

Kalan です。台湾出身で、2019年に日本へ転職し、福岡に住んでいます。フロントエンド開発に精通しているだけでなく、IoT、アプリ開発、バックエンド、電子工作などの分野にも挑戦しています。 最近、エレキギターを始めました。ブログを通じて、より多くの人と交流できればと思っています。気軽に絡んでください