智能捆綁:如何僅向舊版瀏覽器提供舊版代碼

已發表: 2022-03-10
快速總結 ↬儘管在網絡上有效地捆綁資源近來得到了廣泛的關注,但我們向用戶提供前端資源的方式幾乎沒有改變。 網站附帶的 JavaScript 和样式資源的平均權重正在上升——儘管用於優化網站的構建工具從未像現在這樣好。 隨著常青瀏覽器的市場份額快速增長以及瀏覽器同步推出對新功能的支持,我們是時候重新考慮現代網絡的資產交付了嗎?

今天的網站從常青瀏覽器那裡獲得了很大一部分流量——其中大部分都很好地支持了 ES6+、新的 JavaScript 標準、新的 Web 平台 API 和 CSS 屬性。 但是,在不久的將來仍需要支持舊版瀏覽器——它們的使用份額足夠大,不容忽視,具體取決於您的用戶群。

快速瀏覽一下 caniuse.com 的使用表就會發現,常青瀏覽器佔據了瀏覽器市場的最大份額——超過 75%。 儘管如此,規範是為 CSS 加上前綴,將我們所有的 JavaScript 轉換為 ES5,並包含 polyfill 以支持我們關心的每個用戶。

儘管從歷史背景來看這是可以理解的——網絡一直是關於漸進增強的——但問題仍然存在:我們是否會為大多數用戶減慢網絡速度以支持數量不斷減少的舊版瀏覽器?

轉譯為 ES5、Web 平台 polyfills、ES6+ polyfills、CSS 前綴
Web 應用程序的不同兼容性層。 (查看大圖)

支持舊版瀏覽器的成本

讓我們嘗試了解典型構建管道中的不同步驟如何增加我們前端資源的權重:

轉譯為 ES5

為了估計轉譯可以為 JavaScript 包增加多少權重,我選取了一些最初用 ES6+ 編寫的流行 JavaScript 庫,並比較了它們轉譯前後的包大小:

圖書館尺寸
(縮小的 ES6)
尺寸
(縮小的 ES5)
區別
TodoMVC 8.4 KB 11 KB 24.5%
可拖動53.5 KB 77.9 KB 31.3%
盧克森75.4 KB 100.3 KB 24.8%
視頻.js 237.2 KB 335.8 KB 29.4%
PixiJS 370.8 KB 452 KB 18%

平均而言,未轉譯的包比已轉譯到 ES5 的包小 25%。 這並不奇怪,因為 ES6+ 提供了一種更緊湊和更具表現力的方式來表示等效邏輯,並且將其中一些功能轉換為 ES5 可能需要大量代碼。

ES6+ 填充物

儘管 Babel 在我們的 ES6+ 代碼中應用語法轉換方面做得很好,但 ES6+ 中引入的內置特性——例如PromiseMapSet ,以及新的數組和字符串方法——仍然需要填充。 按原樣放入babel-polyfill可以為您的壓縮包增加近 90 KB。

跳躍後更多! 繼續往下看↓

Web 平台 Polyfills

由於大量新的瀏覽器 API 的可用性,現代 Web 應用程序開發得到了簡化。 常用的是fetch ,用於請求資源, IntersectionObserver ,用於有效地觀察元素的可見性,以及URL規範,這使得在 Web 上讀取和操作 URL 更加容易。

為這些功能中的每一個添加符合規範的 polyfill 會對包大小產生顯著影響。

CSS 前綴

最後,讓我們看看 CSS 前綴的影響。 雖然前綴不會像其他構建轉換那樣為捆綁包增加太多的自重——尤其是因為它們在 Gzip 時壓縮得很好——但這裡仍有一些節省空間。

圖書館尺寸
(縮小,最後 5 個瀏覽器版本的前綴)
尺寸
(縮小,最後一個瀏覽器版本的前綴)
區別
引導程序159 KB 132 KB 17%
布爾瑪184 KB 164 KB 10.9%
基礎139 KB 118 KB 15.1%
語義用戶界面622 KB 569 KB 8.5%

發布高效代碼的實用指南

這可能很明顯我要去哪裡。 如果我們利用現有的構建管道將這些兼容性層僅提供給需要它的瀏覽器,我們就可以為其他用戶(佔多數的用戶)提供更輕鬆的體驗,同時保持對舊瀏覽器的兼容性。

現代包比舊包小,因為它放棄了一些兼容性層。
分叉我們的捆綁包。 (查看大圖)

這個想法並不是全新的。 Polyfill.io 等服務試圖在運行時動態填充瀏覽器環境。 但是像這樣的方法有一些缺點:

  • polyfill 的選擇僅限於服務列出的那些——除非您自己託管和維護服務。
  • 因為 polyfill 發生在運行時並且是一個阻塞操作,所以舊瀏覽器上的用戶的頁面加載時間可能會顯著增加。
  • 為每個用戶提供一個定制的 polyfill 文件會給系統帶來熵,當出現問題時,這使得故障排除變得更加困難。

此外,這並不能解決應用程序代碼轉換所增加的權重問題,有時可能比 polyfill 本身更大。

讓我們看看我們如何解決到目前為止我們已經確定的所有臃腫來源。

我們需要的工具

  • 網頁包
    這將是我們的構建工具,儘管該過程將與其他構建工具類似,例如 Parcel 和 Rollup。
  • 瀏覽器列表
    有了這個,我們將管理和定義我們想要支持的瀏覽器。
  • 我們將使用一些Browserslist 支持插件

1. 定義現代和傳統瀏覽器

首先,我們要明確“現代”和“傳統”瀏覽器的含義。 為了便於維護和測試,它有助於將瀏覽器分為兩個獨立的組:將幾乎不需要 polyfill 或 transpilation 的瀏覽器添加到我們的現代列表中,並將其餘瀏覽器放在我們的舊列表中。

火狐 >= 53;邊緣 >= 15;鉻 >= 58; iOS >= 10.1
支持 ES6+、新 CSS 屬性和瀏覽器 API(如 Promises 和 Fetch)的瀏覽器。 (查看大圖)

項目根目錄下的 Browserslist 配置可以存儲此信息。 “環境”小節可用於記錄兩個瀏覽器組,如下所示:

 [modern] Firefox >= 53 Edge >= 15 Chrome >= 58 iOS >= 10.1 [legacy] > 1%

此處提供的列表只是一個示例,可以根據您網站的要求和可用時間進行定制和更新。 此配置將作為我們接下來將創建的兩組前端包的真實來源:一組用於現代瀏覽器,一組用於所有其他用戶。

2. ES6+ 轉譯和Polyfilling

為了以環境感知的方式編譯我們的 JavaScript,我們將使用babel-preset-env

讓我們在項目的根目錄初始化一個.babelrc文件:

 { "presets": [ ["env", { "useBuiltIns": "entry"}] ] }

啟用useBuiltIns標誌允許 Babel 選擇性地填充作為 ES6+ 的一部分引入的內置功能。 因為它過濾了 polyfill 以僅包含環境所需的那些,所以我們降低了使用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. Polyfilling Web 平台功能

要將 web 平台功能的 polyfills 發送給我們的用戶,我們需要為這兩種環境創建兩個入口點:

 require('whatwg-fetch'); require('es6-promise').polyfill(); // … other polyfills

和這個:

 // polyfills for modern browsers (if any) require('intersection-observer');

這是我們流程中唯一需要一定程度手動維護的步驟。 我們可以通過在項目中添加 eslint-plugin-compat 來減少這個過程出錯的可能性。 當我們使用尚未填充的瀏覽器功能時,此插件會警告我們。

4.CSS前綴

最後,讓我們看看如何為不需要它的瀏覽器減少 CSS 前綴。 因為autoprefixer是生態系統中支持從browserslist配置文件讀取的首批工具之一,所以我們在這裡沒有太多工作要做。

在項目的根目錄創建一個簡單的 PostCSS 配置文件就足夠了:

 module.exports = { plugins: [ require('autoprefixer') ], }

把它們放在一起

現在我們已經定義了所有必需的插件配置,我們可以組合一個 webpack 配置來讀取這些配置,並在dist/moderndist/legacy文件夾中輸出兩個單獨的構建。

 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現在應該給我們兩個構建,它們在功能上是等效的。

為用戶提供正確的捆綁包

創建單獨的構建只能幫助我們實現目標的前半部分。 我們仍然需要識別並向用戶提供正確的捆綁包。

還記得我們之前定義的 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的功能之一是確保在匹配用戶代理時考慮平台怪癖。 例如,iOS 上的所有瀏覽器(包括 Chrome)都使用 WebKit 作為底層引擎,並將匹配到各自的 Safari 特定 Browserslist 查詢。

僅僅依靠生產中用戶代理解析的正確性可能並不謹慎。 通過回退到未在現代列表中定義或具有未知或不可解析的用戶代理字符串的瀏覽器的舊捆綁包,我們確保我們的網站仍然有效。

結論:值得嗎?

我們已經設法為我們的客戶提供了一個端到端的流程,用於向我們的客戶運送無膨脹的捆綁包。 但是有理由懷疑這給項目增加的維護開銷是否值得它的好處。 讓我們評估一下這種方法的優缺點:

1. 維護和測試

只需要維護一個為該管道中的所有工具提供支持的 Browserslist 配置。 將來可以隨時更新現代和舊版瀏覽器的定義,而無需重構支持配置或代碼。 我認為這使得維護開銷幾乎可以忽略不計。

然而,依賴 Babel 生成兩個不同的代碼包存在一個小的理論風險,每個代碼包都需要在各自的環境中正常工作。

雖然由於捆綁包中的差異而導致的錯誤可能很少見,但監控這些變體的錯誤應該有助於識別和有效緩解任何問題。

2. 構建時間與運行時間

與當今流行的其他技術不同,所有這些優化都發生在構建時並且對客戶端是不可見的。

3. 逐步提高速度

現代瀏覽器上的用戶體驗變得明顯更快,而舊版瀏覽器上的用戶繼續獲得與以前相同的捆綁服務,沒有任何負面後果。

4. 輕鬆使用現代瀏覽器功能

由於使用它們所需的 polyfill 的大小,我們經常避免使用新的瀏覽器功能。 有時,我們甚至會選擇更小的不符合規範的 polyfill 來節省大小。 這種新方法允許我們使用符合規範的 polyfill,而不必擔心會影響所有用戶。

生產中的差異化捆綁服務

鑑於顯著優勢,我們在為印度最大的家具和裝飾零售商之一 Urban Ladder 的客戶創建新的移動結賬體驗時採用了此構建管道。

在我們已經優化的捆綁包中,我們能夠節省大約 20% 的 Gzip'd CSS 和 JavaScript 資源,這些資源通過網絡發送給現代移動用戶。 因為我們 80% 以上的日常訪問者都使用這些常青瀏覽器,所以付出的努力是值得的。

更多資源

  • “僅在需要時加載 Polyfills”,Philip Walton
  • @babel/preset-env
    一個智能的 Babel 預設
  • 瀏覽器列表“工具”
    為 Browserslist 構建的插件生態系統
  • 我可以用嗎
    當前瀏覽器市場份額表