スマートバンドリング:レガシーブラウザにのみレガシーコードを提供する方法
公開: 2022-03-10今日のWebサイトは、常緑のブラウザーからトラフィックの大部分を受け取ります。そのほとんどは、ES6 +、新しいJavaScript標準、新しいWebプラットフォームAPI、およびCSS属性を適切にサポートしています。 ただし、レガシーブラウザは、近い将来もサポートする必要があります。ユーザーベースによっては、それらの使用シェアは無視できないほど大きいためです。
caniuse.comの使用状況表をざっと見ると、常緑のブラウザがブラウザ市場の大部分を占めており、75%以上を占めていることがわかります。 それにもかかわらず、CSSのプレフィックスを付け、すべてのJavaScriptをES5にトランスパイルし、関心のあるすべてのユーザーをサポートするためにポリフィルを含めるのが一般的です。
これは歴史的な文脈から理解できますが(Webは常にプログレッシブエンハンスメントに関するものです)、疑問が残ります。減少する一連のレガシーブラウザーをサポートするために、大多数のユーザーのWebの速度を落としているのでしょうか。
従来のブラウザをサポートするためのコスト
典型的なビルドパイプラインのさまざまなステップがフロントエンドリソースにどのように重みを加えることができるかを理解してみましょう。
ES5へのトランスパイル
トランスパイルがJavaScriptバンドルに追加できる重みを見積もるために、元々ES6 +で記述された人気のあるJavaScriptライブラリをいくつか取り上げ、トランスパイルの前後のバンドルサイズを比較しました。
図書館 | サイズ (縮小されたES6) | サイズ (縮小されたES5) | 違い |
---|---|---|---|
TodoMVC | 8.4 KB | 11 KB | 24.5% |
ドラッグ可能 | 53.5 KB | 77.9 KB | 31.3% |
Luxon | 75.4 KB | 100.3 KB | 24.8% |
Video.js | 237.2 KB | 335.8 KB | 29.4% |
PixiJS | 370.8 KB | 452 KB | 18% |
平均して、トランスパイルされていないバンドルは、ES5にトランスパイルされたバンドルよりも約25%小さくなっています。 ES6 +が同等のロジックを表現するためのよりコンパクトで表現力豊かな方法を提供し、これらの機能の一部をES5に変換するには多くのコードが必要になる可能性があることを考えると、これは驚くべきことではありません。
ES6 +ポリフィル
BabelはES6 +コードに構文変換を適切に適用しますが、ES6 +で導入された組み込み機能( Promise
、 Map
、 Set
、新しい配列および文字列メソッドなど)は、引き続きポリフィルする必要があります。 babel-polyfill
をそのままドロップすると、縮小されたバンドルに90KB近く追加される可能性があります。
Webプラットフォームポリフィル
多数の新しいブラウザAPIが利用できるようになったため、最新のWebアプリケーション開発が簡素化されました。 一般的に使用されるのは、リソースを要求するためのfetch
、要素の可視性を効率的に監視するためのIntersectionObserver
、およびWeb上でのURLの読み取りと操作を容易にするURL
仕様です。
これらの機能ごとに仕様に準拠したポリフィルを追加すると、バンドルサイズに顕著な影響を与える可能性があります。
CSSプレフィックス
最後に、CSSプレフィックスの影響を見てみましょう。 プレフィックスは、他のビルドトランスフォームほどバンドルに自重を追加することはありませんが、特にGzipで圧縮すると十分に圧縮されるため、ここで達成できる節約がいくつかあります。
図書館 | サイズ (縮小、最新の5つのブラウザーバージョンのプレフィックス) | サイズ (縮小、最後のブラウザバージョンのプレフィックス) | 違い |
---|---|---|---|
ブートストラップ | 159 KB | 132 KB | 17% |
ブルマ | 184 KB | 164 KB | 10.9% |
財団 | 139 KB | 118 KB | 15.1% |
セマンティックUI | 622 KB | 569 KB | 8.5% |
効率的なコードを出荷するための実用的なガイド
私がこれでどこに行くのかはおそらく明らかです。 既存のビルドパイプラインを活用して、これらの互換性レイヤーを必要とするブラウザーにのみ出荷すると、古いブラウザーとの互換性を維持しながら、残りのユーザー(過半数を占めるユーザー)に軽いエクスペリエンスを提供できます。
このアイデアはまったく新しいものではありません。 Polyfill.ioなどのサービスは、実行時にブラウザ環境を動的にポリフィルしようとします。 しかし、このようなアプローチにはいくつかの欠点があります。
- ポリフィルの選択は、サービスを自分でホストおよび保守しない限り、サービスによってリストされたものに制限されます。
- ポリフィルは実行時に発生し、ブロック操作であるため、古いブラウザを使用しているユーザーの場合、ページの読み込み時間が大幅に長くなる可能性があります。
- カスタムメイドのポリフィルファイルをすべてのユーザーに提供すると、システムにエントロピーが導入され、問題が発生した場合のトラブルシューティングが困難になります。
また、これは、アプリケーションコードのトランスパイルによって追加される重みの問題を解決しません。これは、ポリフィル自体よりも大きくなる場合があります。
これまでに特定した膨満感のすべての原因をどのように解決できるか見てみましょう。
必要なツール
- Webpack
これがビルドツールになりますが、プロセスはParcelやRollupなどの他のビルドツールと同様のままです。 - ブラウザリスト
これにより、サポートしたいブラウザを管理および定義します。 - そして、いくつかのBrowserslistサポートプラグインを使用します。
1.最新およびレガシーブラウザの定義
まず、「モダン」ブラウザと「レガシー」ブラウザの意味を明確にします。 メンテナンスとテストを容易にするために、ブラウザーを2つの個別のグループに分割すると便利です。つまり、ポリフィルやトランスパイルをほとんどまたはまったく必要としないブラウザーを最新のリストに追加し、残りをレガシーリストに追加します。
プロジェクトのルートにあるBrowserslist構成で、この情報を保存できます。 「環境」サブセクションは、次のように2つのブラウザグループを文書化するために使用できます。
[modern] Firefox >= 53 Edge >= 15 Chrome >= 58 iOS >= 10.1 [legacy] > 1%
ここに記載されているリストは単なる例であり、Webサイトの要件と利用可能な時間に基づいてカスタマイズおよび更新できます。 この構成は、次に作成する2セットのフロントエンドバンドルの信頼できる情報源として機能します。1つは最新のブラウザー用で、もう1つは他のすべてのユーザー用です。
2. ES6 +トランスパイルとポリフィリング
JavaScriptを環境に対応した方法でトランスパイルするために、 babel-preset-env
を使用します。
プロジェクトのルートにある.babelrc
ファイルを次のように初期化してみましょう。
{ "presets": [ ["env", { "useBuiltIns": "entry"}] ] }
useBuiltIns
フラグを有効にすると、BabelはES6 +の一部として導入された組み込み機能を選択的にポリフィルできます。 ポリフィルをフィルタリングして環境に必要なものだけを含めるため、 babel-polyfill
を使用して輸送コストを完全に軽減できます。
このフラグを機能させるには、エントリポイントにbabel-polyfill
をインポートする必要もあります。
// In import "babel-polyfill";
そうすることで、大規模なbabel-polyfill
インポートが、ターゲットとするブラウザー環境によってフィルター処理された詳細なインポートに置き換えられます。
// Transformed output import "core-js/modules/es7.string.pad-start"; import "core-js/modules/es7.string.pad-end"; import "core-js/modules/web.timers"; …
3.ポリフィリングWebプラットフォーム機能
Webプラットフォーム機能のポリフィルをユーザーに出荷するには、両方の環境に2つのエントリポイントを作成する必要があります。
require('whatwg-fetch'); require('es6-promise').polyfill(); // … other polyfills
この:
// polyfills for modern browsers (if any) require('intersection-observer');
これは、ある程度の手動メンテナンスを必要とするフローの唯一のステップです。 プロジェクトにeslint-plugin-compatを追加することで、このプロセスでエラーが発生しにくくすることができます。 このプラグインは、まだポリフィルされていないブラウザ機能を使用すると警告します。
4.CSSプレフィックス
最後に、CSSプレフィックスを必要としないブラウザのCSSプレフィックスを削減する方法を見てみましょう。 autoprefixer
は、 browserslist
構成ファイルからの読み取りをサポートするエコシステムの最初のツールの1つであったため、ここで行うことはあまりありません。
プロジェクトのルートに単純なPostCSS構成ファイルを作成するだけで十分です。
module.exports = { plugins: [ require('autoprefixer') ], }
すべてを一緒に入れて
必要なプラグイン構成をすべて定義したので、これらを読み取り、 dist/modern
フォルダーとdist/legacy
フォルダーに2つの別々のビルドを出力するwebpack構成をまとめることができます。
const MiniCssExtractPlugin = require('mini-css-extract-plugin') const isModern = process.env.BROWSERSLIST_ENV === 'modern' const buildRoot = path.resolve(__dirname, "dist") module.exports = { entry: [ isModern ? './polyfills.modern.js' : './polyfills.legacy.js', "./main.js" ], output: { path: path.join(buildRoot, isModern ? 'modern' : 'legacy'), filename: 'bundle.[hash].js', }, module: { rules: [ { test: /\.jsx?$/, use: "babel-loader" }, { test: /\.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'] } ]}, plugins: { new MiniCssExtractPlugin(), new HtmlWebpackPlugin({ template: 'index.hbs', filename: 'index.html', }), }, };
最後に、 package.json
ファイルにいくつかのビルドコマンドを作成します。
"scripts": { "build": "yarn build:legacy && yarn build:modern", "build:legacy": "BROWSERSLIST_ENV=legacy webpack -p --config webpack.config.js", "build:modern": "BROWSERSLIST_ENV=modern webpack -p --config webpack.config.js" }
それでおしまい。 ランニングyarn build
は、機能的に同等の2つのビルドを提供するはずです。
ユーザーに適切なバンドルを提供する
個別のビルドを作成すると、目標の前半のみを達成できます。 それでも、適切なバンドルを特定してユーザーに提供する必要があります。
以前に定義したBrowserslist構成を覚えていますか? 同じ構成を使用して、ユーザーがどのカテゴリに分類されるかを判断できたら素晴らしいと思いませんか?
browserslist-useragentと入力します。 名前が示すように、 browserslist-useragent
は、 browserslist
の構成を読み取り、ユーザーエージェントを関連する環境に一致させることができます。 次の例は、Koaサーバーでこれを示しています。
const Koa = require('koa') const app = new Koa() const send = require('koa-send') const { matchesUA } = require('browserslist-useragent') var router = new Router() app.use(router.routes()) router.get('/', async (ctx, next) => { const useragent = ctx.get('User-Agent') const isModernUser = matchesUA(useragent, { env: 'modern', allowHigherVersions: true, }) const index = isModernUser ? 'dist/modern/index.html', 'dist/legacy/index.html' await send(ctx, index); });
ここで、 allowHigherVersions
フラグを設定すると、新しいバージョンのブラウザー(Can I Useのデータベースにまだ含まれていないバージョン)がリリースされた場合でも、最新のブラウザーに対して真実であると報告されます。
browserslist-useragent
の機能の1つは、ユーザーエージェントを照合する際に、プラットフォームの癖が考慮されるようにすることです。 たとえば、iOS上のすべてのブラウザ(Chromeを含む)は、基盤となるエンジンとしてWebKitを使用し、それぞれのSafari固有のBrowserslistクエリと照合されます。
本番環境でのユーザーエージェントの解析の正確さにのみ依存するのは賢明ではないかもしれません。 最新のリストで定義されていない、または不明または解析不可能なユーザーエージェント文字列を持つブラウザのレガシーバンドルにフォールバックすることで、当社のWebサイトが引き続き機能することを保証します。
結論:それは価値がありますか?
膨張のないバンドルをクライアントに出荷するためのエンドツーエンドのフローをカバーすることができました。 しかし、これがプロジェクトに追加するメンテナンスのオーバーヘッドがその利点に値するかどうか疑問に思うのは合理的です。 このアプローチの長所と短所を評価してみましょう。
1.メンテナンスとテスト
このパイプライン内のすべてのツールを強化する単一のBrowserslist構成のみを維持する必要があります。 最新およびレガシーブラウザの定義の更新は、サポートする構成やコードをリファクタリングすることなく、将来いつでも実行できます。 これにより、メンテナンスのオーバーヘッドがほとんど無視できるようになると私は主張します。
ただし、2つの異なるコードバンドルを生成するためにBabelに依存することに関連する小さな理論上のリスクがあり、それぞれがそれぞれの環境で正常に機能する必要があります。
バンドルの違いによるエラーはまれですが、これらのバリアントのエラーを監視すると、問題を特定して効果的に軽減するのに役立ちます。
2.ビルド時間とランタイム
現在普及している他の手法とは異なり、これらの最適化はすべてビルド時に行われ、クライアントには表示されません。
3.プログレッシブエンハンスドスピード
最新のブラウザーのユーザーのエクスペリエンスは大幅に高速化されますが、レガシーブラウザーのユーザーは、悪影響を与えることなく、以前と同じバンドルを引き続き提供されます。
4.最新のブラウザ機能を簡単に使用する
ポリフィルのサイズが大きいため、新しいブラウザ機能の使用を避けることがよくあります。 サイズを節約するために、仕様に準拠していない小さなポリフィルを選択することもあります。 この新しいアプローチにより、すべてのユーザーに影響を与えることをあまり気にせずに、仕様に準拠したポリフィルを使用できます。
本番環境で提供されるディファレンシャルバンドル
重要な利点を考慮して、インド最大の家具および装飾品小売業者の1つであるUrban Ladderの顧客向けに、新しいモバイルチェックアウトエクスペリエンスを作成する際に、このビルドパイプラインを採用しました。
すでに最適化されたバンドルでは、最新のモバイルユーザーに送信されるGzipで圧縮されたCSSおよびJavaScriptリソースを約20%節約することができました。 毎日の訪問者の80%以上がこれらの常緑のブラウザを使用していたため、その努力は影響を与えるだけの価値がありました。
その他のリソース
- 「必要な場合にのみポリフィルをロードする」、Philip Walton
-
@babel/preset-env
スマートなBabelプリセット - Browserslistの「ツール」
Browserslist用に構築されたプラグインのエコシステム - 使ってもいいですか
現在のブラウザの市場シェア表