この記事は、ニフティグループ Advent Calendar 2023 10日目の記事です。
こんにちは。会員システムグループでエンジニアをしている山田良介です。
私の担当するシステムではNext.jsへのシステムリプレースを行い、順調に稼働しています。開発効率向上、テスタビリティの向上など恩恵が大きい一方で、ブラウザサポートの面で課題も見えてきました。
Next.js化での課題
ReactはもともとSPAのためのフレームワークです。HTMLはほぼ空の状態からスタートし、ブラウザ上でJavaScript(React)がほぼ全てのDOMを構築します。
サーバサイドレンダリング(SSR)フレームワークであるNext.jsでもその基本は変わりません。サーバ上で一度JavaScriptを実行してHTMLを構築したのち、ブラウザ上でもう一度JavaScriptを実行し、状態を同期(hydration)することになります。HTMLのDOM全体をJavaScriptが管理している、という状況はReact単体で利用している場合と変わりません。
ここで次の2点が問題になります。
- 未キャッチのエラーが発生した場合、ページ全体が非表示になってしまう
- ブラウザによって対応するJavaScriptの文法が異なるため、予期せぬ文法エラーが起こる
後者が非常に厄介です。「このブラウザはECMAScript20XXまで対応」のような形ならまだ対応しやすいですが、現実には個別の文法ごとに対応状況がバラバラで、全ての文法を網羅的に確認するのは現実的に不可能です。そして文法エラーが発生した場合、前者によりページ全体が落ちてしまいます。
Chrome、Edgeなどのモダンブラウザには自動アップデート機能があるため、基本的に最新版を使っている前提が置けます。しかしSafariはOSごとにバージョンが固定であるため、古いmacOS、iOSの環境でページ全体の表示ができない不具合が発生し、これに悩まされることになりました。
フォールバック対応
まずは緩和策での対応です。
User Agentによる強制リダイレクト
古今すべてのブラウザへ対応するのは不可能です。IE11などサポート対象外とするブラウザを明確に決め、User Agentによる判定でエラーページに遷移させることとしました。
1 2 3 4 5 6 7 8 9 10 |
if (checkSupportedBrowser(ua)) { ... } else { return { redirect: { permanent: false, destination: '/error_page' } } } |
なおエラーページもNext.jsで作ってしまうと、遷移先のエラーページも表示できないという事態に陥るため注意が必要です。
Error Boundaryの設置
Next.jsでエラーを未キャッチにしてしまうとDOM全体がアンマウントされてしまいますが、Error Boundaryを設置することで代替コンポーネントを表示することができます。これは通常のエラー処理でも有用です。
標準機能としては特定のメソッドを実装したコンポーネントを作る必要がありますが、サードパーティーのreact-error-boundaryなどを利用することもできます。
最上位の_app.tsxに最低限設置するほか、必要に応じて各所に挿入することで非表示部分を最小限に抑えることができます。
_app.tsx
1 2 3 4 5 6 7 8 9 |
const MyApp = ({ Component, pageProps }: AppProps): JSX.Element => { return ( <ErrorBoundary FallbackComponent={() => <DefaultFallbackPage />}> <Component {...pageProps}> </ErrorBoundary> ); }; export default MyApp; |
ただしErrorBoundaryコンポーネントそのものやフォールバック先のコンポーネントも文法エラーになってしまうことがあるため、レガシーブラウザ対応という観点では100%安全ではありません。
JavaScriptダウングレード対応
JavaScriptのトランスパイルにより古いブラウザに対応させることで、対応するブラウザの幅を広げる対応です。
トランスパイルの設定
TypeScriptの出力ターゲットをサポートしたいブラウザの対応範囲まで下げます。
tsconfig.json
1 2 3 4 5 6 |
{ "compilerOptions": { "target": "es5" ... } } |
またpackage.jsonでbrowserslistを指定することで、トランスパイル時に対応ブラウザが考慮されるようにします。文法はplaygroundで確認すると良いでしょう。
Next.jsの場合、webpackでのJavaScriptおよびCSS Modulesのトランスパイル結果に影響します。
package.json
1 2 3 4 5 6 7 8 9 |
{ ... "browserslist": [ "> 0.5%", "Safari >= 11", "iOS >= 11", ... ] } |
transpilePackagesの指定
Next.jsはデフォルト設定のままだと、プロジェクト内のソースのみをトランスパイルし、node_modulesをトランスパイルしません。従って、ライブラリで新しい文法が使われている場合に対応できません。
node_modules内のパッケージをトランスパイルするためには、next.config.jsでtranspilePackagesを指定します。
next.config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
const transpilePackages = [ '@tanstack/query-core', '@tanstack/react-query', 'ky', 'zod', ]; /** @type {import('next').NextConfig} */ const nextConfig = { ... transpilePackages: transpilePackages } module.exports = nextConfig; |
一括で全てトランスパイル対象にするようなオプションはなく、対象とするパッケージ名を個別に指定する必要があります。
なおjestで next/jestを使用している場合、この設定はjestのtransform設定にも反映されます。
polyfillの追加
上記設定ではトランスパイルで解決できる文法が修正されますが、polyfillが必要な文法に対応できません。エラーとなる文法を特定した上で、polyfill.ioを利用することで対処します。
_document.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
const Document = (): JSX.Element => { return ( <Html> <Head /> <body> <Main /> <NextScript /> <Script src="https://polyfill.io/v3/polyfill.min.js?features=globalThis" strategy="beforeInteractive" /> </body> </Html> ); }; export default Document; |
polyfill.ioはUser Agentを元にブラウザごとにpolyfillを返してくれるサービスです。featuresでpolyfill対象の文法を指定することができます。上記例ではglobalThisに対するpolyfillが返ります。
調査環境の整備
上記対応は一度設定すれば恒久的に使えるようなものではありません。
改修やパッケージアップデートなどで要対応箇所が増える可能性があるため、継続的なメンテナンスが必要です。そのためにはデバッグ環境を用意する必要があります。
ブラウザテストサービスの利用
レガシーブラウザでの検証を実機で行うのは困難です。古いOSを搭載した実機を、確認したいブラウザバージョン数の分だけ用意する必要があります。またOSが古いことによるセキュリティリスクもあります。
このため、リモートでブラウザを借りて利用できるSaaSを利用することにしました。代表的なところだとBrowserStackやSauce Labsなどがあります。ブラウザ経由でSaaS側が用意したブラウザを操作することができ、開発者ツールなども使用可能です。
サービスによっては日本国内のサーバがなかったり、日本語入力に難があったりなどの問題はあるものの、最低限の動作確認はできるようになりました。
自動検知
リリースのたびにレガシーブラウザまで手動検証しなければならないのは面倒なので、なんとかして自動化したいところです。上記に挙げたサービスでは各種E2Eテストツールにも対応しているため、CIでの自動化ができそうです。これは現在検証中です。
ただしレガシーブラウザを対象とすることを考えると、以下の制約があります。
- ツールはSelenium一択状態
- JavaScript文法エラーそのものを検出することはできない
CypressやPlaywrightといった最近のE2Eテストツールは操作対象のブラウザバージョンが指定されており、基本的に最新のものを要求します。従ってレガシーブラウザ対応では利用できません。WebDriver対応ブラウザに対して汎用的に使えるSeleniumか、そのラッパーが現状唯一の選択肢になっています。
また文法エラーはブラウザコンソールログへの出力で確認が可能ですが、コンソールログを取得する機能はWebDriverの規格にはありません。Chromeの独自機能としては存在しますが、Safariなどでは利用できません。このため、エラー検知は画面要素の検証によって行い、具体的なエラー内容は別途手動でデバッグするしかなさそうです。
また、Sentryなどのエラートラッキングツールを利用することでも検知は可能です。ただしSentryに到達する前に文法エラーでJavaScriptの読み込みが停止した場合、検知不可能になります。
まとめ
Next.jsでのレガシーブラウザ対応についてご紹介しました。
Next.jsに限らず、JavaScriptを多用するモダンなフロントエンドフレームワークは互換性の問題を抱えがちです。サポートするブラウザのポリシーを明確にし、検証体制を整えた上で臨むようにしましょう。