DevTools のアーキテクチャ更新: DevTools から TypeScript への移行

Tim van der Lippe
Tim van der Lippe

この投稿は、DevTools のアーキテクチャに加える変更とその構築方法について説明する一連のブログ投稿の一部です。

JavaScript モジュールへの移行Web Components への移行に続き、本日は、Devtools のアーキテクチャに加える変更とその構築方法に関するブログ投稿シリーズを継続していきます。(まだご覧になっていない場合は、DevTools のアーキテクチャを最新のウェブにアップグレードする作業に関する動画を投稿し、ウェブ プロジェクトを改善する 14 のヒントをご覧ください)。

この投稿では、Closure Compiler のタイプチェッカーから TypeScript に移行する 13 か月間の歩みについて説明します。

はじめに

DevTools コードベースのサイズと、それに取り組むエンジニアに自信を与える必要性を考えると、型チェッカーの使用が不可欠です。そのため、2013 年に DevTools は Closure Compiler を採用しました。Closure を採用したことで、DevTools のエンジニアは自信を持って変更を加えることができます。Closure コンパイラは型チェックを実行して、すべてのシステム統合が適切に型付けされていることを確認します。

しかし、時が経つにつれて、最新のウェブ開発では代替型チェッカーが普及しました。代表的な例として、TypeScriptFlow の 2 つが挙げられます。さらに、TypeScript は Google の公式プログラミング言語になりました。これらの新しい型チェッカーの人気が高まっている一方で、Google は、型チェッカーがキャッチするはずのリグレッションを送信していることにも気づきました。そのため、タイプチェッカーの選択を再評価し、DevTools での開発の次のステップを考えることにしました。

型チェッカーの評価

DevTools はすでにタイプチェッカーを使用していたため、次の質問に答える必要があります。

Closure Compiler を使い続けるか、新しい型チェッカーに移行するか。

この質問に答えるために、いくつかの特性について型チェッカーを評価する必要がありました。型チェッカーの使用はエンジニアの信頼度に重点を置いているため、最も重要な側面は型の正確性です。言い換えれば、タイプチェッカーは本当の問題を発見する上で、どの程度の信頼性があるかということです。

評価は、リリースした回帰と、その根本原因の特定に重点を置きました。ここでは、すでに Closure Compiler を使用していたため、Closure はこれらの問題を捕捉しなかったと仮定します。したがって、他のタイプのチェッカーが処理可能かどうかを判断する必要があります。

TypeScript での型の正確性

TypeScript は Google で公式にサポートされているプログラミング言語であり、急速に普及しているため、Google はまず TypeScript を評価することにしました。TypeScript は興味深い選択肢でした。TypeScript チーム自体が、JavaScript の型チェックとの互換性を追跡するためのテスト プロジェクトの 1 つとして DevTools を使用しています。 同社のベースライン参照テストの出力から、TypeScript が大量の型の問題、つまり Closure コンパイラが必ずしも検出していない問題を検出していることがわかりました。これらの問題の多くは、リリースしたリグレッションの根本原因である可能性が高いため、TypeScript が DevTools で実現可能なオプションになり得ると考えました。

JavaScript モジュールへの移行時には、Closure Compiler がこれまで以上に多くの問題を発見していることに気づきました。標準のモジュール形式に移行することで、Closure がコードベースを理解する能力が向上し、その結果、型チェッカーの効果が向上しました。 しかし、TypeScript チームは、JavaScript モジュールの移行に先立って、DevTools のベースライン バージョンを使用していました。そのため、JavaScript モジュールへの移行により、TypeScript コンパイラが捕捉するエラーの量も減少したかどうかを確認する必要がありました。

TypeScript の評価

DevTools は 10 年以上前から存在しており、今ではかなりのサイズで機能豊富なウェブ アプリケーションにまで成長しました。このブログ投稿の執筆時点で、DevTools には約 15 万行のファースト パーティ JavaScript コードが含まれています。 ソースコードで TypeScript コンパイラを実行したとき、膨大な量のエラーに圧倒されました。 TypeScript コンパイラがコード解決に関連するエラー(約 2,000 件のエラー)を出力する回数は減ったものの、コードベースには型の互換性に関連するエラーがさらに 6,000 件あることがわかりました。

これにより、TypeScript は型の解決方法を理解できましたが、コードベースには多数の型の非互換性があることが判明しました。 これらのエラーを手動で分析したところ、TypeScript は(ほとんどの場合)正しいことがわかりました。TypeScript がこれらを検出でき、Closure も検出できなかった理由は、Closure コンパイラが型を Any と推測することが多いのに対し、TypeScript は割り当てに基づいて型推論を行い、より正確な型を推測するためでした。 そのため、TypeScript の方が確かにオブジェクトの構造をよりよく理解し、問題のある使用法を発見できました

その問題の 1 つに、DevTools での Closure コンパイラの使用において @unrestricted が頻繁に使用されていたというものがあります。クラスに @unrestricted アノテーションを付けると、その特定のクラスに対する Closure コンパイラの厳密なプロパティ チェックが実質的にオフになります。つまり、デベロッパーは型安全性なしでクラス定義を自由に拡張できます。DevTools コードベースで @unrestricted の使用が広まっている理由に関する過去のコンテキストは見つかりませんでしたが、コードベースの大部分で、Closure コンパイラが安全性の低いモードで実行されていました。

回帰と TypeScript が発見した型エラーとのクロス分析でも重複が見られ、(型自体が正しい場合であれば)TypeScript はこれらの問題を防止できた可能性があると考えました。

any 通話を発信しています

この時点で、Closure Compiler の使用方法を改善するか、TypeScript に移行するかを決める必要がありました。(Flow は Google でも Chromium でもサポートされていないため、このオプションを使用しない必要がありました)。 JavaScript/TypeScript ツールに取り組んでいる Google のエンジニアとの話し合いと推奨を踏まえ、TypeScript コンパイラを採用することにしました。 (先日、Puppeteer から TypeScript への移行に関するブログ投稿も公開しました)。

TypeScript コンパイラを使用する主な理由は、型の正確性の向上でしたが、その他のメリットとしては、Google 社内の TypeScript チームによるサポートや、(JSDoc の typedefs ではなく)interfaces などの TypeScript 言語の機能が挙げられます。

TypeScript コンパイラを選択すると、DevTools のコードベースとその内部アーキテクチャに多大な投資を行う必要がありました。そのため、TypeScript への移行には少なくとも 1 年が必要であると推定しました(2020 年第 3 四半期を目標)。

移行を実行する

残された最大の疑問は、TypeScript にどのように移行するかというものでした。15 万行のコードがあり、一度に移行することはできません。また、コードベースで TypeScript を実行すると、何千ものエラーが見つかることもわかっていました。

複数のオプションを評価しました。

  1. すべての TypeScript エラーを取得し、「ゴールデン」出力と比較します。このアプローチは、TypeScript のチームと同様のアプローチです。このアプローチの最大の欠点は、何十人ものエンジニアが同じコードベースで作業しているため、マージの競合が頻繁に発生することです。
  2. 問題のあるタイプをすべて any に設定する。これにより、TypeScript は基本的にエラーを抑制します。移行の目標は型の正確性であり、抑制によって損なわれるため、このオプションは選択しませんでした。
  3. すべての TypeScript のエラーを手動で修正します。これには何千ものエラーを修正する必要があり、時間がかかります。

多大な労力が予想されたにもかかわらず、オプション 3 を選択しました。この選択肢を選んだ理由は他にもあります。たとえば、すべてのコードを監査し、その実装を含むすべての機能を 10 年に 1 回レビューできるなどでした。ビジネスの観点では、新しい価値を提供するのではなく、現状を維持しています。このため、選択肢 3 を正しい選択肢として正当化することがさらに難しくなっていました。

しかし、Google は TypeScript を採用することで、特に回帰に関する問題を防ぐことができると確信していました。そのため、「新しいビジネス価値を付加している」ではなく、「獲得したビジネス価値を失わないように徹底する」という主張が重視されています。

TypeScript コンパイラの JavaScript サポート

同意を得て、同じ JavaScript コード上で Closure コンパイラと TypeScript コンパイラの両方を実行する計画を策定した後、いくつかの小さなファイルから始めました。 Google のアプローチはほぼボトムアップでした。コアコードから始めて、アーキテクチャの上位レベルのパネルにたどり着くまで進めてきました。

DevTools のすべてのファイルに @ts-nocheck を予防的に追加することで、作業を並列化できました。「TypeScript を修正する」プロセスでは、@ts-nocheck アノテーションを削除し、TypeScript が検出するエラーをすべて解決します。つまり、各ファイルがチェック済みで、できる限り多くの種類の問題が解決されているという確信を持つことができました。

一般的に、この方法ではうまくいく問題はほとんどありませんでした。TypeScript コンパイラでいくつかのバグが見つかりましたが、ほとんどは見つけられていませんでした。

  1. any を返す関数型の省略可能なパラメータは、必須として扱われます。#38551
  2. クラスの静的メソッドにプロパティを割り当てると宣言が中断されます: #38553
  3. 引数のないコンストラクタを含むサブクラスと args コンストラクタを含むスーパークラスを宣言した場合、子コンストラクタが省略される: #41397

これらのバグは、99% の場合において、TypeScript コンパイラがビルドの基盤となる強固な基盤であることを示しています。はい。このような曖昧なバグが DevTools で問題を引き起こすこともありますが、ほとんどの場合はわかりにくく、簡単に回避できました。

混乱を引き起こした唯一の問題は、.tsbuildinfo ファイルの非決定的な出力でした(#37156)。Chromium では、同じ Chromium commit の 2 つのビルドがまったく同じ出力になる必要があります。 残念ながら、Google の Chromium ビルド エンジニアは .tsbuildinfo の出力が非決定的なものであることがわかりました(crbug.com/1054494)。 この問題を回避するには、.tsbuildinfo ファイル(基本的には JSON が含まれています)にモンキーパッチを適用し、決定論的な出力を返すように後処理する必要がありました。https://crrev.com/c/2091448 幸いなことに、TypeScript チームがアップストリームの問題を解決し、すぐに回避策を取り除くことができました。TypeScript チームには、バグレポートを受け付けて迅速に修正していただき、ありがとうございました。

全体として、TypeScript コンパイラの(型)正確性に満足しています。 大規模なオープンソース JavaScript プロジェクトである Devtools が、TypeScript での JavaScript サポートの定着に役立ったことを願っています。

被害の統計を分析する

これらの型エラーの解決を順調に進め、TypeScript でチェックするコードの量を徐々に増やすことができました。 しかし、2020 年 8 月(この移行から 9 か月後)に確認を行ったところ、現在のペースでは期限に間に合わないことがわかりました。Google のエンジニアの 1 人が、「TypeScriptification」(この移行に付けられた名前)の進行状況を示す分析グラフを作成しました。

TypeScript の移行の進行状況

TypeScript の移行の進行状況 - 移行が必要な残りのコード行を追跡する

残りのゼロラインに達する推定は、2021 年 7 月から 2021 年 12 月までであり、期限をほぼ 1 年過ぎていました。経営陣や他のエンジニアと話し合った結果、TypeScript コンパイラ サポートへの移行に取り組むエンジニアを増やすことに合意しました。 これは、複数の異なるファイルで作業する複数のエンジニアが互いに競合しないように、移行が並列化できるように設計されていたためです。

この時点で、TypeScriptification のプロセスはチーム全体の取り組みとなりました。追加の支援により、2020 年 11 月末に移行を完了することができました。これは開始から 13 か月後、当初の予測よりも 1 年以上前でした。

合計で、18 人のエンジニアが 771 の変更リスト(pull リクエストと同様)を提出しました。Google のトラッキング バグ(https://crbug.com/1011811)には 1,200 件を超えるコメントがあり、そのほとんどは変更リストから自動的に投稿されます。 Google のトラッキング シートには、入力対象となるすべてのファイル、その割り当て先、およびそれらが「入力済み」となっている変更リストについて、500 行を超える行がありました。

TypeScript コンパイラのパフォーマンスの影響を軽減する

現在私たちが対処している最大の問題は、TypeScript コンパイラのパフォーマンスの低下です。Chromium と DevTools を構築するエンジニアの人数を考えると、このボトルネックには費用がかかります。残念ながら、移行前にこのリスクを特定できませんでした。また、大部分のファイルを TypeScript に移行した時点になって初めて、Chromium ビルド全体で費やされる時間が大幅に増加したことが判明しました(https://crbug.com/1139220)。

Microsoft TypeScript コンパイラ チームにアップストリームでこの問題を報告しましたが、残念なことに、この動作は意図的なものであると判断されました。 この問題が再検討されることを願っていますが、当面は、Chromium 側でのパフォーマンス低下の影響を可能な限り軽減するよう努めています。

残念ながら、現在利用できるソリューションは、Google 社員以外の貢献者には必ずしも適していません。Chromium へのオープンソースのコントリビューションは非常に重要です(特に Microsoft Edge チームからのコントリビューションによるもの)。そのため、Google では、すべてのコントリビューターが役立つ別の方法を積極的に探しています。しかしながら、現時点では適切な代替ソリューションが見つかっておりません。

DevTools の TypeScript の現在の状態

現時点では、Closure コンパイラ タイプ チェッカーをコードベースから削除し、TypeScript コンパイラのみを使用するようになりました。TypeScript で作成されたファイルを作成し、TypeScript 固有の機能(インターフェース、ジェネリックなど)を利用できるため、日常的に役立ちます。TypeScript コンパイラが型エラーや回帰を検出できるという確信が持てるようになりました。これは、Google がこの移行に初めて着手したときに実現したいと望んでいました。他の多くの企業と同様、この移行は時間のかかる作業であり、微妙な差異があり、困難なこともありましたが、メリットが得られれば、移行には価値があると確信しています。

プレビュー チャネルをダウンロードする

Chrome CanaryDevBeta を既定の開発ブラウザとして使用することをご検討ください。これらのプレビュー チャンネルでは、最新の DevTools 機能にアクセスしたり、最先端のウェブ プラットフォーム API をテストしたり、ユーザーが実際に体験する前にサイト上の問題を検出したりできます。

Chrome DevTools チームへのお問い合わせ

投稿内の新機能や変更点、または DevTools に関するその他のことについて話し合うには、次のオプションを使用します。

  • crbug.com からご提案やフィードバックをお送りください。
  • DevTools の問題を報告するには、DevTools でその他のオプション アイコン その他   > [ヘルプ] > [DevTools の問題を報告する] を選択します。
  • @ChromeDevTools にツイートします。
  • 「DevTools の新機能」の YouTube 動画または DevTools のヒントの YouTube 動画でコメントを残してください。