プロンプトとコンテキスト
フロントエンド基盤チームが大規模な TypeScript アプリケーションを保守しています。あるアナリティクスモジュールが、インポート時にグローバルリスナーを登録し、環境設定を読み込んでいるため、起動が遅延しています。チームは TypeScript 5.9 の import defer を使用してこれらの副作用を遅延させたいと考えていますが、その成果物は構文を理解できないレガシーランタイム上でも動作し続ける必要があります。ロード、評価、アクセスによるトリガー、および移行ゲートについて説明してください。
TypeScript 5.9 のノートでは、import defer は名前空間インポートのみを許可すると記載されています。モジュールとその依存関係は先にロードできますが、モジュールコードの評価は名前空間のメンバーにアクセスした際に行われます。TypeScript はこの構文をダウンレベル変換しないため、直接の保持は preserve または esnext モジュールモードを対象としています。
面接官が見ているポイント
面接官は、ロードと評価の違い、静的インポートと動的インポートの違い、およびトップレベルの副作用に関する理解を求めています。優れた回答では、import defer を単なる遅延読み込み(lazy loading)の短縮記法として扱うのではなく、バンドラー、レガシーブラウザ、SSR、プリロード、テストの分離、ロールバックについても網羅します。
最初に明確にすべき質問
- ターゲットのランタイムとバンドラーは
import deferをネイティブに解釈できるか? - モジュールの副作用は安全に後回しにできるか、それとも起動時に発生する必要があるか?
- どのメンバーアクセスが評価のトリガーとなり、隠れたトップレベルの読み取りは存在しないか?
- SSR、クライアントハイドレーション、プリロード、テストは決定論的な順序を必要としているか?
- 移行が失敗した場合、通常の静的インポートや動的
import()に戻せるか?
30秒での回答
「私は import defer を動的ロードではなく『遅延評価(deferred evaluation)』として定義します。TypeScript 5.9 では名前空間インポートが必要であり、リソースはロードされる可能性がありますが、モジュールが評価されるのは最初の名前空間メンバーへのアクセス時です。コンパイラはレガシーランタイム向けの変換を提供しません。私ならトップレベルの副作用と SSR の順序を監査し、対応するバンドラーとランタイムで小規模な実験を行い、通常のインポートや動的 import() への切り替え手段を維持します。ビルド、ハイドレーション、パフォーマンス、副作用のテストに合格した後にのみ適用を拡大します。」
ステップバイステップの詳細解説
1. セマンティクスとバージョンを固定する
TypeScript 5.9 のバージョン、モジュールモード、TC39 プロポーザルのステータスを記録します。ロードと評価は別々の事象です。import defer は評価を遅延させるものであり、必ずしもネットワークリクエストを遅延させるわけではありません。起動時のウォーターフォールから実行を推測するのではなく、両方のタイムスタンプを計測します。
2. 構文の境界を明確にする
評価を遅延できるのは名前空間インポートのみであり、デフォルトインポートや名前付きインポートは無効です。名前空間のプロパティへのアクセスが評価のトリガーとなるため、その読み取りが観測可能な実行境界となります。
import defer * as analytics from "./analytics.js";
// The module is loaded, but top-level registration has not run.
export function openPanel() {
analytics.start(); // First member access triggers evaluation.
}呼び出し側が analytics.start より前にグローバル登録を必要とする場合、遅延評価によって動作が変化します。通常のインポートを維持するか、初期化を明示的な関数に移行してください。
3. dynamic import と比較する
動的 import() は通常 Promise を返し、ロードと評価を非同期フローに配置します。import defer は静的なモジュール関係を維持し、リソースを早期にロードできる可能性があり、トップレベルの実行を遅延させます。エラーの伝播、プリロード、コード分割、SSR、テストのタイミングが異なるため、両者を同一視すると誤ったパフォーマンス比較につながります。
4. 副作用とアクセスグラフを監査する
イベントリスナー、シングルトンの登録、環境変数の読み取り、ポリフィル、テレメトリの初期化、キャッシュの初期投入など、トップレベルの副作用をリストアップします。ハイドレーション、ルートのプリフェッチ、テストのセットアップを含め、名前空間メンバーへのすべてのアクセスを追跡します。順序が重要な場合は、呼び出し側がタイミングを選択できるように副作用を明示的な initialize() に移行します。
5. ビルドとランタイムの互換性を処理する
TypeScript は import defer をダウンレベル変換しません。レガシーランタイムの場合、バンドラーがそれを変換できるか、あるいは拒否するかを確認します。それができなければ通常のインポートまたは動的 import() の実装を維持します。CI では、対象ブラウザ、Node SSR、開発サーバー、本番バンドル、ソースマップ、コード分割、エラーバウンダリをカバーする必要があります。
6. ロールアウトとロールバックのゲートを設定する
遅延、静的、動的パス用にフィーチャーフラグを使用します。初回インタラクションのレイテンシ、評価時間、重複初期化、ハイドレーションエラーを記録します。順序の変更、レガシーランタイムの構文エラー、メトリクスの悪化が発生した場合はフラグを無効にし、プロポーザルやツールチェーンが進化している間は安定したインポートパスに戻します。
模範回答
まず TypeScript 5.9、バンドラー、および各ターゲットランタイムのサポートマトリクスを確認し、次にトップレベルの副作用を洗い出します。import defer は名前空間インポートをサポートし、評価前にモジュールをロードでき、最初のメンバーアクセス時に評価します。TypeScript はダウンレベル変換を提供しません。私ならロードと評価を個別に計測し、SSR、ハイドレーション、レガシーブラウザ、バンドル、テスト分離、重複初期化をテストし、フィーチャーフラグによる通常のインポートへのフォールバックを維持します。順序、成果物、パフォーマンスデータが安定した後にのみ移行を進めます。
よくある間違い
import deferを動的import()として扱う → 静的依存関係と Promise のタイミングを見落とす → ロード、評価、エラーを個別に計測する。- デフォルトまたは名前付きインポートを使用する → TypeScript 5.9 の構文境界に違反する → 名前空間インポートを使用し、アクセス時にトリガーする。
- TypeScript が書き換えてくれると思い込む → レガシーランタイムで構文エラーが発生する可能性がある → バンドラーのサポートを確認し、フォールバックを維持する。
- トップレベルの副作用を無視する → リスナー、ポリフィル、テレメトリの順序が変わる → 初期化を明示的にするか、通常のインポートを維持する。
- 起動時のネットワークのみを見る → 早期ロードは早期実行を意味しない → 両方のタイムスタンプを記録する。
関連する追加質問
なぜ名前空間インポートが必要なのですか?
名前空間オブジェクトは、明確なプロパティアクセス境界を提供するためです。デフォルトインポートや名前付きインポートはセットアップ中に具体的なバインディングを公開するため、「最初のメンバーアクセス時に評価する」という統一ルールを維持できません。
コード分割(code splitting)と同じですか?
いいえ。コード分割はリソースのパッケージ化とロードの方法を制御します。import defer は主にモジュールコードが評価されるタイミングを変更します。リソース自体はすでにロードまたはプリフェッチされている可能性があります。
SSR ではどのように処理すべきですか?
サーバーとクライアントの評価順序の互換性を維持します。クライアントが登録していない状態でサーバーがグローバル状態を登録すると、ハイドレーションの不整合(不一致)が発生する可能性があります。必要に応じてサーバー側では静的インポートを維持するか、初期化を明示的かつ反復可能にします。
1回限りの副作用をどのようにテストしますか?
分離されたテストプロセスにおいて、アクセスなし、初回アクセス、反復アクセス、同時アクセス、破棄(teardown)にわたるモジュール初期化をカウントします。シングルトンやリスナーが重複して登録されていないことをアサートします。
どのような場合に避けるべきですか?
ランタイムやバンドラーのサポートがない場合、起動時の副作用を後回しにできない場合、SSR の順序を変更できない場合、またはパフォーマンス向上が再現できない場合は避けてください。代わりに通常のインポートまたは動的 import() を使用します。