ブラウザ拡張機能の開発 with Github Copilot ~タブ管理の困ったを1つ解決してみた~

はじめに

こんにちは。NewITソリューション部です。
日々の業務でWEBブラウザを使用している人は多いと思いますが、気がつくとタブが増えすぎてしまい、タブ管理に困った経験はありませんか?
私自身、日頃何度も使用するサイトはブックマーク等に登録はすれど、そのタブは開いたままにしておくことが多く、気がつくとタブの数がどんどん増えてしまったといったことがよくあります。
特に、同じサイトのタブが複数開いていたり、もう使わないような長時間未使用なタブがずっと残っていたりすると、PCやブラウザのパフォーマンスにも影響が出てしまいます。


本記事では、こうしたタブ管理の困りごとを解決するために、GitHub Copilotを活用してブラウザ拡張機能をスピード開発したプロセスと得られた知見を紹介します。タブ管理だけでなく、日頃の業務効率化や生産性向上に役立つようなブラウザ拡張機能の開発の参考にもなると思いますので、ぜひご覧ください。

今回解決した「困った」ポイント

前提として、私はブックマークからサイトを開くより、そもそもタブを開きっぱなしにするスタイルを好んでいます。
そのため、今回解決したいポイントとしては、タブの整理ではなく、現状のままメモリパフォーマンスを改善することです。


そこで、以下の2つの困ったを解決することにしました。

  1. 重複タブの存在: タブの数が膨大になると、意図せず重複して存在するタブに気づくことが困難になります。これを解消するべく重複するタブの視覚化及び、探す/閉じる機能を実装しました。
  2. 長時間未使用タブの存在: また使う or 使うかもしれない等の理由で閉じることをしなかったタブというのは日々積み重なり、気がづくと膨大な数になってしまいます。デフォルトのブラウザ機能として、タブのメモリパフォーマンスを調整する機能が備わっていることはありますが、任意のタイミングで機能させることはできません。これを解消するべく、今回は24h以上未使用のタブを一覧化し、一括休止状態にすることでのメモリ解放の機能を実装しました。

開発した拡張機能の概要

今回開発したのは、サイドパネルで動作するシンプルなタブ管理拡張機能です。主な特徴は以下の通りです。

  • 現在開いているタブの数を表示
  • 重複しているタブの視覚化
  • 24時間以上使用していないタブの視覚化&休止状態化
重複放置タブはどれかを一目で把握し、不要なタブを簡単に調整できるようにしました。

実装

  • plasmoフレームワークの利用: 実装とビルドプロセスの効率化のため、ブラウザ拡張機能開発に特化したplasmoフレームワークを使用しました。これにより、複雑な設定を省き、迅速な開発が可能になります。
  • GitHub Copilotの活用: React + TypeScriptベースで実装を行うにあたって、UIやロジックの全てをGithub Copilotで自動生成することで、開発スピードを大幅に向上させました。
  • サイドパネルのみのシンプルな構成: 複雑な設定や多機能化は避け、まずは「困った」を解決する最小限の機能に絞って実装しました。
  • 開発プロセス: 機能要件の整理 → 開発環境 + plasmoプロジェクト作成 → CopilotでUI/ロジック実装 → 動作確認/修正という流れで進めました。

プロジェクトのセットアップ

まずは開発環境を整えるため、plasmoフレームワークを使用してプロジェクトをセットアップしました。以下のコマンドで新しいプロジェクトを作成します。

pnpm create plasmo

# init完了後、プロジェクトディレクトリに移動する
cd <project-directory>

機能の実装

今回は複雑なプロジェクト構成の一切を省き、1ファイルにサイドパネルの機能を実装しました。

主にchrome.windows APIを使用して、現在のウインドウのタブ情報を取得したり、chrome.tabs APIを使用して、リアルタイムにタブのステータス取得やタブの操作を行います。

以下に実装した主要ロジックを紹介します。
※ソースコードは、GitHub Copilotによって生成された物をそのまま使用しています。そのため、至らぬ点があるかもしれませんが、ご了承ください。

// sidepanel.tsx
// 現在のウインドウのタブ情報を取得する
const getCurrentWindow = async () => {
  await chrome.windows.getCurrent(
    {
      populate: true
    },
    (currentWindow) => {
      const currentTabs = currentWindow.tabs ? currentWindow.tabs : []
      setTabs(currentTabs) // 現在の全てのタブ情報を状態に保存
      setDuplicateTabs(findDuplicateTabs(currentTabs)) // 重複タブを検出し状態に保存
    }
  )
}

// URLが重複するタブを検出する
const findDuplicateTabs = (tabs: chrome.tabs.Tab[]) => {
  const urlMap = new Map<string, number[]>()
  const duplicates: number[] = []

  tabs.forEach((tab) => {
    if (tab.url) {
      const ids = urlMap.get(tab.url) || []
      if (ids.length > 0) {
        duplicates.push(...ids)
        duplicates.push(tab.id!)
      }
      urlMap.set(tab.url, [...ids, tab.id!])
    }
  })

  return [...new Set(duplicates)]
}

// 長時間未使用のタブを検出する
const findOldTabs = (tabs: chrome.tabs.Tab[]) => {
  const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000
  return tabs.filter((tab) => {
    const lastAccessed = (tab as any).lastAccessed
    return lastAccessed && lastAccessed < oneDayAgo && !tab.active
  })
}
const findNonDiscardedOldTabs = (tabs: chrome.tabs.Tab[]) => {
  return findOldTabs(tabs).filter((tab) => !tab.discarded)
}

// サイドパネル内のクリックイベントハンドラー - 選択したタブに移動する
const handleTabClick = (tabId: number) => {
  chrome.tabs.update(tabId, { active: true })
  getCurrentWindow()
}
// サイドパネル内のクリックイベントハンドラー - 長時間未使用のタブを休止状態にする
const handleDiscardOldTabs = async () => {
  const nonDiscardedOldTabs = findNonDiscardedOldTabs(tabs || [])
  await Promise.all(
    nonDiscardedOldTabs.map(async (tab) => {
      await chrome.tabs.discard(tab.id!)
    })
  )
  await getCurrentWindow()
}

// サイドパネル内のクリックイベントハンドラー - 選択したタブを閉じる
const handleTabClose = async (event: React.MouseEvent, tabId: number) => {
  event.stopPropagation()
  await chrome.tabs.remove(tabId)
  await getCurrentWindow()
}

// サイドパネルの初期化
useEffect(() => {
  getCurrentWindow()
}, [])

// イベントリスナーの登録
chrome.tabs.onCreated.addListener((tab) => {
  getCurrentWindow()
})
chrome.tabs.onRemoved.addListener((tabId, removeInfo) => {
  getCurrentWindow()
})
// background.ts
export {}

// 拡張機能アイコンをクリックすることでサイドパネルが開閉できるように設定
chrome.sidePanel
  .setPanelBehavior({ openPanelOnActionClick: true })
  .catch((error) => console.error(error))

完成した拡張機能

  • Open Tabs: 現在のウインドウで開いているタブ数
  • Duplicate Tabs: 重複しているタブの数
  • Inactive(24+): 24時間以上利用のないタブ数
    • Not Suspended: 上記の内休止状態にないタブ数
  • Suspend Inactive Tabs: Inactive(24+)の全てを休止状態にするボタン
  • 以下、Duplicate/Inactive(24+) Tabsの一覧

今後の展望

実際に使用してみて、重複タブの視覚化は非常に便利だと感じました。サイドパネル上でクリックすることで遷移する機能も備えているので、どこにあるかが一目瞭然で良かったです。
しかし、肝心のメモリパフォーマンス向上に向けた休止状態にする機能は思った以上の効果は得られませんでした。これはブラウザのデフォルト機能としてある程度のメモリ効率化がされているため、その上に重ね掛けする形になり、効果が出なかったと推測されます。
ですが、最近使用していないタブの視覚化ができたことで、真に不必要なタブを探すことは容易になりました。

改善案として、任意の時間を指定できるようにすることで、直近開いていたタブでも休止状態への移行を可能とすることで、高いメモリ解放を実現できると考えます。また、タブ毎のメモリ使用率などを可視化し、任意にタブを選択し休止状態にすることも高い効果が得られると考えます。
また、デフォルトでは得られないタブ情報を視覚化するこの有効性は一定あると考え、今後はタブの整理に着目した拡張機能へと方針を進めたいと思いました。

さいごに

今回の様に、単純かつ少量の機能であれば、Github Copilotを活用することで、開発スピードを大幅に向上させることができました。特に、UIの実装やAPIの使用方法、ロジックの実装については機能要件を指示するだけで、Copilotが適切なコードを提案してくれるため、手動でのコーディングをすることなく、ほぼ自動生成で実装を完了することができました。
日頃から頻繁に使用するブラウザに対する今回の様なアプローチは、例え些細な機能であっても積み重ねていくことで、業務効率化や生産性向上に大きく寄与するのではないかと予感しました。何しろ、開発にかかるコストをAIによって大幅に削減可能になった今の時代、些細な「アレが欲しい」の実現ハードルが大幅に下がったと感じています。
ブラウザ拡張機能は、サーバーといったインフラ等が必須ではないため、気軽に開発と利用が可能な点も魅力的です。個人の日常の便利さを追求することはもちろん、社内における業務効率化のためのツール開発にも適していると感じています。


最後に、弊社にご興味をお持ちいただけましたらお気軽にお問い合わせいただけると幸いです。

いいね (←参考になった場合はハートマークを押して評価お願いします)
読み込み中...

注意事項・免責事項

※技術情報につきましては投稿日時点の情報となります。投稿日以降に仕様等が変更されていることがありますのでご了承ください。

※公式な技術情報の紹介の他、当社による検証結果および経験に基づく独自の見解が含まれている場合がございます。

※これらの技術情報によって被ったいかなる損害についても、当社は一切責任を負わないものといたします。十分な確認・検証の上、ご活用お願いたします。

※当サイトはマイクロソフト社によるサポートページではございません。パーソルクロステクノロジー株式会社が運営しているサイトのため、マイクロソフト社によるサポートを希望される方は適切な問い合わせ先にご確認ください。
 【重要】マイクロソフト社のサポートをお求めの方は、問い合わせ窓口をご確認ください