静的解析を利用したレガシーブラウザ対応

OGP

はじめに

この記事はenechain Advent Calendar 2024の7日目の記事になります。

こんにちは、enechainでソフトウェアエンジニアとして働いている@sue71です。現在は2024年10月9日にリリースした「eSquare Live」の開発を担当しています。

「eSquare Live」は、電力卸取引の自動取引プラットフォームであり、10月にローンチしたenechainの新規プロダクトです。現在は発電事業者や電力小売事業者を含む多くのお客様にご利用いただいています。 開発の裏側や技術スタックに関してはこちらの記事で紹介しているので是非ご覧ください。

本記事では、そんなeSquare Liveを安定して提供するための「レガシーブラウザ対応」について解説します。

eSquare Liveのブラウザ対応要件

eSquare Liveでは開発当初、セキュリティとパフォーマンスを重視し、ChromeとEdgeの最新2バージョンをサポート対象としていましたが、お客様の属性が明らかになるにつれブラウザ要件の見直しが求められるようになりました。

大小さまざまな組織のお客様に利用いただいている中で、PCの管理方針やセキュリティポリシーによってブラウザを自由に更新できないケースも見受けられました。これを踏まえ、他サービスのGoogle Analyticsのアクセスログを基準に過去2年間のバージョンをサポート範囲とする方針に変更しました。

JavaScriptのレガシーブラウザ対応

まず、旧ブラウザ対応の基本としてJavaScriptの構文やAPIの対応状況を確認する必要があります。 構文に関してはトランスパイラにより殆どのケースが対応可能ですが、JavaScriptのAPIは個々に確認する必要があります。

静的解析を利用したPolyfill対応

近年の旧ブラウザ対応では、browserslistとトランスパイラ(例: BabelやSWC)を組み合わせた静的解析によって必要なPolyfillを自動追加するアプローチが一般的です。この方法では対象ブラウザがサポートしていないAPIを検出し、不足分を補完するコードをBundleに含めることができます。 browserslistとは、サポート対象のブラウザをクエリ形式で指定し、トランスパイラやCSSツールと共有するための設定ツールです。以下は @babel-preset/envと組み合わせた場合の例です。

babel.config.json:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "entry",
        "corejs": "3.22"
      }
    ]
  ]
}

.browserslistrc:

> 0.25%
not dead

browserslistの設定ファイルにブラウザのバージョンを示すクエリを記述することでトランスパイル時に利用箇所でcorejsのimport文が自動的に挿入されます。

eSquare Liveでは、軽量で高速な開発環境を構築するためにViteを利用しています。 Viteはトランスパイラとしてesbuildを採用しており、高速なビルドが可能です。 ただし、esbuild自体には非対応APIの検出やPolyfillの追加機能が含まれていないため、別途対応が必要です。 その解決方法として、以下のようなアプローチが考えられます。

  • SWC, Babelの機能を利用してトランスパイル時にPolyfillを読み込むように変換する
  • eslintなどのlinterで静的に検証しサポートしていないAPIの利用を制限する

コミュニティプラグインとしてvite-plugin-react-swcが提供されておりeSquare Liveでも開発時に利用しています。 そこでSWCのenvを利用できないかと考えるかもしれませんが、執筆時点ではSWC関連の設定はハードコーディングされているためユーザーは自由に設定できません。

@vitejs/plugin-legacyの利用

Viteは旧ブラウザ対応のためにコミュニティプラグインとして@vitejs/plugin-legacyを提供しています。

基本は前項で紹介した@babel/preset-envを利用した方法ですが、加えて以下の特徴があります:

  • レガシーブラウザとモダンブラウザでchunkを分割し動的に読み込むchunkを分けている
  • core-js(Polyfill)へのimport文を取り除いてPolyfill専用のchunkとして分割して生成

またViteでは、レガシーブラウザ、モダンブラウザの区分はnative ESMの対応状況を基準としています。 今回のブラウザ対応要件ではnative ESM対応バージョン以降の物のみが対象であるため、このままだとPolyfillが読み込まれないことになります。 そこで定常的にPolyfillを読み込むようにするためには以下のパラメータを適切に設定する必要があります。

  • modernTargetsで対応ブラウザの範囲を明示
  • modernPolyfillsでモダンブラウザでもPolyfillが読み込まれるように

またこの方法を取る場合は最新のブラウザでも同じようにPolyfillのコードを読み込んでしまうことに注意してください。

If modernTargets is not set, it is not recommended to use the true value (which uses auto-detection) because core-js@3 is very aggressive in polyfill inclusions due to all the bleeding edge features it supports. Even when targeting native ESM support, it injects 15kb of polyfills!

と公式のドキュメントでも説明されているように、modernTargetsで範囲を明示しない場合、native ESMのサポートを基準としたターゲットが指定されるため非常に大きなサイズのPolyfillが生成されることになります。 modernTargetsで必要最低限の範囲を明示するか、https://cdnjs.cloudflare.com/polyfill などのサービスの利用も検討してください。

CSSのレガシーブラウザ対応

開発当初は最新2バージョンを見越していたため、CSSの比較的新しいプロパティや値を利用していました。 例えば、板情報を表示するUIでは比較的新しい仕様であるsubgridを利用して構築していました。 しかしsubgridの対応バージョンはEdgeおよびChromeともに117であり、 リリース当時(2023年9月12日)の2年前という制約を満たしていなかったためレイアウト崩れが発生していました。

これらの対応状況はMDNCan I useなどで簡単に確認できるものの、開発時に検証する仕組みが欲しくなります。 JavaScriptとは違ってCSSの場合はPolyfillなどの後付けで再現することが難しいことを考えると、eslintなどのlinterで利用を防ぐしかなさそうです。

@mdn/browser-compat-dataの利用

MDNはWebアプリケーション以外に@mdn/browser-compat-dataというパッケージでブラウザの対応状況を示した構造化データを公開してくれています。 eSquare Liveではこのデータベースを利用した独自のESLint Pluginで静的に検証するアプローチを取ることにしました。

以下は@mdn/browser-compat-dataから引用したJSONファイルの一部でgrid-template-columnsの対応状況を示したものです。 各パラメータと指定可能な値がそれぞれキーとなって定義されており、それぞれのキーに追加されたバージョン、削除されたバージョンが記されています。 mirrorとは対応するバージョンをアップストリームのブラウザの対応バージョンに追従する、という意味でその対応関係もこちらのJSONファイルで公開されています。

{
  "css": {
    "properties": {
      "grid-template-columns": {
        "__compat": {
          "mdn_url": "https://developer.mozilla.org/docs/Web/CSS/grid-template-columns",
          "spec_url": [
            "https://drafts.csswg.org/css-grid/#track-sizing",
            "https://drafts.csswg.org/css-grid/#subgrids"
          ],
          "tags": [
            "web-features:grid"
          ],
          "support": {
            "chrome": {
              "version_added": "57"
            },
            "chrome_android": "mirror",
            "edge": [
              {
                "version_added": "16"
              },
              {
                "alternative_name": "-ms-grid-columns",
                "version_added": "12",
                "version_removed": "79"
              }
            ],
            ...
          },
          "status": {
            "experimental": false,
            "standard_track": true,
            "deprecated": false
          }
        },
        ...
        "subgrid": {
          "__compat": {
            "mdn_url": "https://developer.mozilla.org/docs/Web/CSS/CSS_grid_layout/Subgrid",
            "spec_url": "https://drafts.csswg.org/css-grid/#subgrids",
            "tags": [
              "web-features:subgrid"
            ],
            "support": {
              "chrome": {
                "version_added": "117"
              },
              "chrome_android": "mirror",
              "edge": "mirror",
              ...
            },
            "status": {
              "experimental": false,
              "standard_track": true,
              "deprecated": false
            }
          }
        }
      }
    }
  }
}

このJSONと対応するブラウザのバージョンを突き合わせることで、利用しているプロパティ、または値が最も古い対応ブラウザで対応されているかを検証できそうです。

browserslistの利用

サービスのブラウザ対応バージョンの識別子を利用するために前節で軽く触れたbrowserslistを利用します。browserslistはクエリを書くことで対応するブラウザの識別子を配列で出力してくれます。 わかりやすさのためにlast 2 yearsとしていますが、実際の運用では時と共に変化してしまうため詳細なバージョン情報を記載する方が良いでしょう。

const targetBrowsers = browserlist("last 2 years") // 出力例: ["edge 107", "chrome 107", ...]

ESLint Ruleへの組み込み

eSquare Liveでは @chakra-ui/react (以下Chakra UI)を利用しているのでstyleはChakra UIのprops経由で指定されています。 そこでeSquare LiveではChakra UIのpropsから実際に利用されるCSSプロパティを取得し、前述のMDNのデータと比較するアプローチを取っています。 詳細な実装は省きますが、下記の流れでJSXOpeningElementの各ノードを検証しています。

  1. browserslistにてターゲットブラウザを識別子を取得
  2. JSXOpeningElementの各ノードがChakra UIによって提供されているコンポーネントの場合propsを取り出す
  3. 2の情報から実際に利用されるCSSプロパティを得る
  4. 3と@mdn/browser-data-compatから得られるJSONと突き合わせることで対象ブラウザの中から非対応のブラウザを抽出する
  5. 対象ブラウザの中から非対応のブラウザが見つかった場合、lint errorとして報告する
export const chakraCSSPropertyCompat: TSESLint.RuleModule<'unsupportedCSSProperty'> = {
  meta: {
    ...
    messages: {
      unsupportedCSSProperty:
        'CSS property "{{property}}" with "{{value}}" is not supported in some target browsers: {{browsers}}.',
    },
  },
  defaultOptions: [
    ...
  ],
  create(context) {
    const targets = context.options[0]?.targets
    const targetBrowsers = browserslist(targets)
    ...
    return {
      JSXOpeningElement(node: TSESTree.JSXOpeningElement) {
        ...
        context.report({
            node: attribute,
            messageId: 'unsupportedCSSProperty',
            data: {
              property: propertyName,
              value: value?.type === AST_NODE_TYPES.Literal ? value.value : '',
              browsers: unsupportedBrowsers.join(', '),
            },
        })
      },
    }
  },
}

以下のスクリーンショットは実際に利用した際のものです。 IDEで開発中にもバージョン違反しているCSSのプロパティや値が確認できるようになりました。

出力例

この辺りのアプローチに慣れた方であればお分かりかもしれませんが、このツールの対応は限定的です。 例えば subgrid のような値がJSXExpressionContainerになっている場合など、動的に変更されるケースではlinterのアプローチでは対応できません。 linterによって開発時に気づける確率は格段に上がりますが、JavaScriptやCSS以外でもブラウザ間で挙動が異なるケースは数多くあります。 そこでeSquare Liveではブラウザの動作確認環境も用意しています。

実際のブラウザの確認環境

eSquare Liveでは前述の開発ツールに加えて開発時のテストや問題が起きた際の調査用にBrowserStackを試験的に導入しています。

BrowserStackは以下の特徴を持っているSaaSです:

  • クラウド上で様々なブラウザ環境をテスト可能
  • ローカル環境とトンネル接続して実運用環境を再現可能

BrowserStackの他にも類似サービスはいくつかありますが、選定理由に関しては割愛します。 旧いブラウザ、特に2年前のバージョンともなるとVMを利用してもローカル環境で再現するのは中々に骨が折れますが、BrowserStackを利用することで簡単に実環境とほぼ同等の状態でテストできるようになりました。

まとめ

今回は以下の内容についてお話しました:

  • eSquare Liveのブラウザ対応方針
  • JavaScript APIおよびCSSの対応方法
  • 静的検証ツールの利用やテスト環境による補完

toBのツールなどではブラウザ対応が必要になるケースが比較的多いものの、各ブラウザをすべて完璧にテストするのは難しい状況もあります。 今回はそんな面倒なブラウザ対応をできるだけコストを掛けずに行う方法について解説しました。レガシーブラウザ対応に苦しむフロントエンドエンジニアの助けになれば幸いです。

enechainでは、事業拡大のために共に技術力で道を切り拓いていく仲間を募集しています。

herp.careers

herp.careers