日本語版
最新ニュース
科学&テクノロジー

Airbnb がスムーズにアップグレードする方法 | Andre Wiggins | Airbnb テック ブログ | 2024 年 7 月

ダウングレードせずに最新のReact機能を展開するために、フロントエンドインフラストラクチャを段階的に近代化します。Airbnb のフロントエンドは最近、大きな節目を迎えました。すべての Web サーフェスが React 16 から React の現在のメジャー バージョンである React 18 にアップグレードされました¹。これは、ゲスト ページとホスト ページ、多くの内部ツールなど、多くのサーフェスを持つ製品にとって大きなプロジェクトでした。このアップグレードを安全に実行するために、私たちは React アップグレード システムを作成しました。これは、モノレポ全体に新しいバージョンの React を段階的に展開し、アップグレードの結果を測定できる再利用可能なインフラストラクチャです。このブログ投稿では、私たちのアップグレード哲学、作成したシステム、そしてこのアップグレードの実行から学んだことについて説明します。この投稿は主に React に焦点を当てていますが、システムとレッスンは、定期的なアップグレードが必要な多くの Web フレームワークとライブラリに適用できます。アップグレードの課題依存関係のアップグレードは、長期にわたるプロジェクトでは一般的なタスクです。アップグレードにより、バグが修正され、パフォーマンスが向上し、新しい API がアンロックされます。アップグレードは簡単なものもありますが、大量の製品コードが変更された API や動作に関する微妙な仮定に依存している場合は、アップグレードが難しくなります。Airbnb の Web モノレポでは、各トップレベルの依存関係のバージョンは 1 つだけ許可され (まれな例外もあります)、リポジトリのルートには 1 つの package.json があります。これにより、モノレポ内のコードが内部的に互換性と一貫性を保ち、ユーザーに重複したパッケージを配布することがなくなります。アップグレード…

Airbnb がスムーズにアップグレードする方法 | Andre Wiggins | Airbnb テック ブログ | 2024 年 7 月

1722654540
2024-08-02 12:15:16

ダウングレードせずに最新のReact機能を展開するために、フロントエンドインフラストラクチャを段階的に近代化します。

Airbnb のフロントエンドは最近、大きな節目を迎えました。すべての Web サーフェスが React 16 から React の現在のメジャー バージョンである React 18 にアップグレードされました¹。これは、ゲスト ページとホスト ページ、多くの内部ツールなど、多くのサーフェスを持つ製品にとって大きなプロジェクトでした。このアップグレードを安全に実行するために、私たちは React アップグレード システムを作成しました。これは、モノレポ全体に新しいバージョンの React を段階的に展開し、アップグレードの結果を測定できる再利用可能なインフラストラクチャです。このブログ投稿では、私たちのアップグレード哲学、作成したシステム、そしてこのアップグレードの実行から学んだことについて説明します。

この投稿は主に React に焦点を当てていますが、システムとレッスンは、定期的なアップグレードが必要な多くの Web フレームワークとライブラリに適用できます。

アップグレードの課題

依存関係のアップグレードは、長期にわたるプロジェクトでは一般的なタスクです。アップグレードにより、バグが修正され、パフォーマンスが向上し、新しい API がアンロックされます。アップグレードは簡単なものもありますが、大量の製品コードが変更された API や動作に関する微妙な仮定に依存している場合は、アップグレードが難しくなります。Airbnb の Web モノレポでは、各トップレベルの依存関係のバージョンは 1 つだけ許可され (まれな例外もあります)、リポジトリのルートには 1 つの package.json があります。これにより、モノレポ内のコードが内部的に互換性と一貫性を保ち、ユーザーに重複したパッケージを配布することがなくなります。アップグレード システムが導入される前は、 シングルバージョン 各依存関係のアトミック アップデートを実行するには、膨大な事前の移行作業、長期にわたるアップグレード ブランチ、および最終的にユーザーに展開される単一のマイルストーンが必要になります。このようなアプローチはエラーが発生しやすくリスクが高いため、クリーンなアップグレードを出荷するには「英雄的な」エンジニアリング作業が必要になります。

理想的には、問題のない小規模な段階的アップグレードを出荷します。このシステムをテストし、大規模なモノレポに段階的に展開する方法がなければ、問題が見つかるたびにダウングレードして、アップグレードを何度も試す必要があることがよくあります。このアップグレード戦略では、パフォーマンスの低下を捉えることが特に困難でした。リリース前にパフォーマンス データを収集する方法がなかったため、展開時に 0% から 100% に直接展開しました。

React のメジャー バージョンとマイナー バージョンの理想と現実の推移を示すグラフです。

React アップグレード システムの目標は、よりシームレスなアップグレードを、それほど難しくなく、より日常的なものにすることでした。具体的には、次のことを実現することを目標としました。

  1. 段階的にアップグレードする できるだけ早くフィードバックを得て、教訓を学べるようにします。
  2. 頻繁にアップグレードする 私たちのバージョンとアップグレードされたバージョン間の差分が可能な限り小さくなるようにするためです。
  3. アップグレードをテストする これにより、アップグレードによるパフォーマンスへの影響を正確に測定し、このデータを使用してアップグレード パスに関する情報に基づいた決定を下すことができます。

これらの目標から逆算して、理想的なアーキテクチャがどのようなものになるかを考え始めました。段階的にアップグレードできるように、長期間実行されるアップグレード ブランチを避けたいと考えていました。また、アップグレードを A/B テストして、本番環境からのフィードバックを得て出荷の決定に役立てたいと考えていました。

理想的なアップグレードシステムの簡略図

このシステムの最も単純な実装では、解決すべき問題がいくつかありました。レンダリングには React の 1 つのバージョンを選択する必要があり、実行時に 2 つのバージョンを動的に切り替えるのは困難でした。この単純なアプローチを使用して基本的なアプリケーションをレンダリングする場合のコードは次のようになります。

import React18 from 'react'; 
import React16 from 'react'; // duplicated import?

if (shouldEnableReact18()) {
const root = React18.createRoot(container);
root.render();
} else {
React16.render(, container);
}

これには 2 つの問題があります。

  1. アプリケーションにReactの両方のバージョンをバンドルすることは望ましくありません。そうしないと、フレームワークバンドルのサイズが2倍になります。さらに、ビルド時に使用されているJSX変換を変更する必要があるかもしれません。 どちらかのバージョンと互換性がありません。
  2. インポート元がどこなのかは明確ではありません。「react」依存関係は React 16 または React 18 のいずれかを指しますが、両方を指すことはありません。

これらの問題を解決するために、私たちは モジュールエイリアシング バージョンを分割し、 環境ターゲティング React の 2 つの分割バージョンをビルドして実行します。

モジュールエイリアシング

これらのインポートがどこから来ているのかという問題は、モジュールエイリアシングを使用して解決しました。yarnを使用して、package.jsonに別のReact依存関係を追加しました。

"react-18": "npm:react@18"

これにより、React を ‘react-18’ パッケージからインポートできるようになりました。これで、目的の一部を達成できました。多くのツール (カスタム リゾルバやビルド システムなど) では、2 つのバージョンのどちらを使用するかを認識する必要があります。ロジックを一元化するために、すべてのカスタム ツールを中央の「グローバル エイリアス」構成にまとめました。このグローバル エイリアス構成により、さまざまなツールすべてを 1 か所でエイリアスできるようになりました。Babel、Jest™、Webpack™、その他のカスタム解決ロジックはすべて、インポートを ‘react’ から ‘react-18’ にリダイレクトする条件を認識する必要があります。モジュールを「グローバル エイリアス」構成でエイリアスすると、ユーザー コードをまったく変更する必要がなく、このリダイレクトをバックグラウンドで処理できるようになります。

TypeScript の矛盾

どのコンポーネントも React 16 または 18 で実行できるため、アップグレード期間中は両方のバージョンで機能する各コンポーネントの型を使用したいと考えました。ありがたいことに、React チームはメジャー バージョン間でも下位互換性を維持しています。

React 18用の型をインストールし、React 18で新しく追加されたAPIについては、React 16と18の両方で機能するこれらのAPI用のshimレイヤーを作成しました(たとえば、 トランジションを使用する 16ではno-opとして動作しました。シムが存在しないAPI(例えば、 useId)、このフックは実行時に未定義になる可能性があることを型拡張によって示しました。

TypeScriptのみ React 18 の重大な変更、React 18 へのアップグレードが完了するまで待ってから、段階的に修正しました。モノレポでこれらの新しい Typescript エラーを段階的に修正できるように、違いを修正する型を拡張しました。

環境ターゲティング

重複インポートの問題を解決するために、2つの異なるビルド成果物を作成する必要がありました。1つはReact 16を含み、もう1つはReact 18を含みます。これらをそれぞれ「コントロール」と「トリートメント」の成果物と呼びます。Airbnbはサーバーサイドレンダリング(SSR)を使用しているため、サーバー上の異なるノードプロセスでこれら2つの異なる成果物を実行する必要もありました。Kubernetes®を使用して、これらのコントロール成果物とトリートメント成果物を実行する2つの異なるKubernetes環境を設定しました。この設定を次のように呼びます。 環境ターゲティング

モジュールエイリアシング そして 環境ターゲティング 異なるバージョンのフレームワークを本番環境で一緒に展開するために一緒に使用する

また、ビルド時にアセットに環境変数 (REACT_UPGRADE) を書き込み、実行時にノード SSR サービスでこの変数を設定しました。これにより、アップグレード システムの一方または他方の側でのみ必要となる条件付きロジックを実行できます。

この設定は、ローカル開発でも機能しました。「ローカル」開発環境もデプロイされていたため、この設定を使用して、本番環境と同じようにローカル開発用の React バージョンを構成することができました。各 SSR サービスが React 18 にアップグレードされたので、そのサービスの開発環境も React 18 に切り替えて、本番環境とローカル開発バージョンの同期を維持しました。

アップグレードのテスト

Airbnb には包括的なテスト スイートがあり、ユーザーにアップグレードを公開する前に、このアップグレードの安全性に対する信頼を築くのに役立ちました。テスト スイートには、視覚的な回帰テスト、統合テスト、ユニット テストが含まれています。ユーザーにリリースする前に、これらの各スイートで発生したすべての新しい障害を修正しました。

ユニットテストはフレームワーク内部から抽象化するのがもっとも困難でした。 酵素と反応テストライブラリの組み合わせ、ユニットテスト、シム、アダプターにおける API とフレームワーク内部に関する想定を修正する必要がありました。これを実現するために、すべてのユニットテストを React 16 と 18 の両方で実行し、React 18 テスト スイートの既存の障害を段階的に修正しながら許容しました。この「許容される障害」リストを使用して、時間の経過とともにテストの失敗の数を減らし、リストに新しい障害が許可されないようにすることで、後退を防止しました。このアプローチにより、コンポーネントとテスト環境の問題を段階的に修正することができました。

私たちはダッシュボードで何百ものテスト失敗を解決する作業を追跡し、アップグレード システムを使用して段階的に修正をマージし、少数の開発者間で作業を分割しました。これにより、移行作業はフロントエンド チーム全体にとってほぼ透明になり、ロールアウト前にアップグレードに対する自信を得ることができました。

段階的な展開

かつて私たちは モジュールエイリアシング そして 環境ターゲティング、私たちは同じコードベースから 2 つの異なるバージョンの React のコードを作成し、配信することができました。安全性とテスト可能性を確保するには、この新しい環境を段階的に展開する方法も必要でした。一度に発生する変更の量を減らすために、トラフィックと製品サーフェス全体で展開を制御する必要がありました。実験インフラストラクチャにより、2 つの本番環境 (制御と処理) のそれぞれにトラフィックを自由に送信できました。この設定により、最初に社内でアップグレードをテストし、問題が見つかった場合はアップグレードを完全に無効にすることもできました。

異なるサーフェスへのロールアウトを制御するのはより困難です。シングル ページ アプリ内で複数の React バージョンを管理するには、React ルートのマウントとアンマウントが必要になります。これによりパフォーマンスが低下し、ユーザー エクスペリエンスが低下します。

このため、私たちは Surface のロールアウト アップグレードをアプリ レベルで管理しました。Airbnb のモノレポには多くのシングル ページ アプリが含まれているため、これらのアプリごとにアップグレードのオン/オフを切り替えることができる React アップグレード システムを導入しておくと便利です。React アップグレード システムを使用することで、まず社内で 1 つのアプリにロールアウトし、開発サイトとステージング サイトの両方でテスト用にアップグレードをオプトインおよびオプトアウトする方法を開発者に提供できました。このアプローチにより、機能ブランチを長期間実行せずに済み、段階的なアップグレードの目標を達成できました。

このシステムを使用して、AirbnbのすべてのWebサーフェスにReact 18を完全に展開しました。ロールバックは必要ありませんでした。アップグレード後、次のような新しいAPIのテストを開始することができました。 新しいルートAPI そして 同時レンダリング機能アップグレードが落ち着くまで、これらの機能の導入を意図的に数週間延期しました。こうすることで、ダウングレードしてコードの変更を元に戻す必要がないと確信できました。

これらの新機能の導入によるパフォーマンスの向上は喜ばしいことであり、私たちは、メリットが得られる主要な UI サーフェスにこれらの機能を拡張する実験を続けています。

アップグレードの目標が確実に達成されるように、Reactアップグレードシステムを使用してテストを行います。 ReactのカナリアチャンネルReact 18を指す代わりに、カナリアタグを指すだけで、現在どのような移行作業が必要なのかをプレビューできます。 リアクト19アップグレードに多大な労力を必要としないようにするには、最新の状態を維持することは、一度限りの大規模な変更ではなく、長期間にわたる継続的な努力である必要があります。

Reactアップグレードシステムの目標は、 段階的にアップグレードし、アップグレードをテストする、 そして 頻繁にアップグレードしてください。 環境ターゲティングとエイリアス システムを組み合わせることで、段階的にアップグレードし、アップグレードをテストできるようになりました。React 19 ベータ版に対してフロントエンドを実行し始めており、React 19 で先行しています。

React のバージョン間、さらにはメジャー バージョン間の下位互換性に尽力した React チームに感謝の意を表します。その努力がなければ、このアップグレード アプローチは不可能でした。

React アップグレード システムを使用することで、React 18 の展開に自信が持てるようになり、今後のアップグレードにもこのアプローチを採用する予定です。アップグレードは時間の経過とともに必要になるため、アップグレード システムへの投資は価値があると考えています。React アップグレード システムにより、アップグレードを段階的にテストして展開できるようになり、ユーザーに可能な限り最高のユーザー エクスペリエンスとパフォーマンスを提供できるようになりました。

このような仕事に興味があるなら、 募集中の役職 — 採用中です!

感謝します ジョシュア・ネルソン React アップグレード システムの構築を主導し、このブログ投稿を執筆してくれたことに感謝します。

さらに、このシステムとそれを構成する各部分に関して支援してくれた Kim Nguyen、Callie Riggins Zetino、James Robinson、Dan Beam、Kaeson Ho、Rae Liu、Michael James、Noah Sugarman、Laurie Jin、Brie Bunge、Matt Mulder、Victor Lin にも感謝します。

#Airbnb #がスムーズにアップグレードする方法 #Andre #Wiggins #Airbnb #テック #ブログ #年 #月

執筆者について: nipponese

Nipponese News編集部は、国内外のニュースを日本語で分かりやすくお届けします。