Polyfill 策略與 bundle 大小的爭鬥

Polyfill 允許我們在舊版本的環境使用新穎的 API ,但是錯誤配置 Polyfill 的方式,只會讓 Modern Browser 白白浪費網路資源,那麼在高流、一刻不能遲、一個 User 也不能少的生產環境有什麼樣的策略呢?

寫在文前:如果你從來沒發現 IE11 看不懂超多 ES6 語法 or 你壓根可以控制 User browser 的版本(如 Hybrid App),那麼這篇對你的幫助可能不大。

如果我們知道 Feature code 裡頭只有用到 ProxyObject.keysArray.from

當我們明確知道使用的語法哪些是需要 Polyfill 時,我們可以僅 import 對應的 Polyfill 進來,多數的 Polyfill 會像是這樣:

if (!Array.from) {
Array.from = (function () {
/* Using the ES5 method to implement this method... */
}
}

(關於 Array.from 的詳細 Polyfill,請查看此處。)

沒錯,Polyfill 在開始前其實會確認該語法是不是早就存在。(JavaScript 如此動態到令人吐槽的特性,在解決跨版本語法問題時,竟顯得特別友好,哈。

於是乎,你可以載入 Proxy Object.keysArray.from ,又不會讓你的 modern browser 用戶的 Built-in API 爛掉。

But,如果你今天要載入的 Polyfill 一狗票又很大的時候…

你的 Chrome user 這時會私訊你:“What the fuck?我開你們網站前置作業一堆是怎樣?而且第一行 if 就走了內,Hello?”

當你的 Team 全部都是沒經歷 IE 時代的小哥哥與小姐姐,這時除了需要 built-time 先轉譯的 Syntax (如箭頭函數)以外,其他 ES6 API 通通要 Polyfill 時…

如同上面那方法最後說的,你的 Chrome user 如果按下 F12 會很想打電話到你的分機把你幹死

這時你就會開始想:我能不能 by browser 決定最終 user 看到的 polyfill.js 內容?

《金融時報》(Financial Times)工程師們想到這樣的解法:

讓我們在 Server-side 先偷瞄一下使用者的瀏覽器是啥,再把對應的 Polyfill 回傳回來…這樣做在 IE11 就能拿到完整的 Polyfill ,而像是 Chrome 這樣的瀏覽器,我們就給他一個全空的 script 也就無需多餘的流量了!

這就是 Financial-Times 所開源的 Polyfill service 核心精神 https://github.com/Financial-Times/polyfill-service

Polyfill.io is a service which makes web development less frustrating by selectively polyfilling just what the browser needs. Polyfill.io reads the User-Agent header of each request and returns polyfills that are suitable for the requesting browser.

當你的使用者不知道裝了什麼牛逼的瀏覽器,結果 user-agent 亂成一團,或是大家都以為你看得懂 Array.from ,但是在其他腳本 or extension 跑完後被 pollution 成undefined…

FYI 第一種方法非常的肥大,對 modern browser 相當不友好。

這時有另外一個想法如果我們只把判定 API 是否存在的 code 放進來,跑完一輪判定後,再從 client-side 去拉需要 Polyfill 的 patch 回來呢?

這樣的做法相當有趣,並且一定程度解決了第一個方法所造成的問題。而我是在 Web Component 的社群發現這樣子的 Practice 。

在 Web Component 中,browser 一定要看懂的幾個 API:customElementshadowDomHTMLTemplateElementPromiseArray.from…族繁不及備載。

而他們的 polyfill loader 的介紹這樣寫到: The webcomponents-loader.js is a client-side loader that dynamically loads the minimum polyfill bundle, using feature detection.

實際上是怎麼做到的呢?

L107–114:是feature detection,但是 detect 到的時候不是直接去執行 polyfill (那時根本沒有載入), 而是 push 到一個名為 polyfills 的 array 裡頭。

L141–155:一系列的 Polyfills array 所記錄缺少的,透過 script 標籤插入到 browser 中實現動態載入。

(有沒有一種果然魔術被破解後感覺:啊原來就這樣啊…的感覺,哈。)

休息一下,在下文中我們會比較不同做法,以及探討如何實現在生產環境。

你現在可能心想:“gan,你說了那麼多,所以到底哪個好?法三嗎?還是二?

其實這個問題還真的不一定!每一個方法都有其好與壞,以及 implement 到生產環境的做法,以下針對其分析。

看似肥大的法一,在前提不一樣時未必是爛方法

我們都知道 babel 、 autoprefixer 可以透過 browserlist 決定要 Patch 的 property prefix、 method 。

當你的 Project 在後台 User 的 browser 都是非常新穎、或是跟著 mobile OS 在走的時候,其實你的 Browserlist 設好,拉進來的 Polyfill 可能非常的小,小到不需要花費 Effort 去優化

優秀的 Server-side solution 如何整合進現有的 App

你可以直接使用 Polyfill service,或是自己 build 一個起來(http://localhost:8080/v3/polyfill.min.js)。

思路如下:

法一、建立一個 microservice 給 polyfill service,在生產環境使用 .min.js ,將其 service 的位置放在 Site 的 head 裡頭。

法二、建立一個 internal-service,在 render 前 forward user request去轉打,並且 async 幹進去 App code 的頂端。

優點:在 Client 端,對這些操作甚至完全無感(Client 無需 feature detect),直接拿到對應的 Polyfill 。

缺點:Server loading 顯然比較重,且不同架構各有優缺。

新穎的 Client-loader 與其優缺

將不同的 polyfill code spliting 到 assets 中,並且在 Client-side 做 feature detect 去決定要拉去的 assets 進來。

缺點:請求數過多、任一腳本 timeout 就會 crash、風險上升。

優點:對於 modern browser 完全無感、無 server logic、透過 HTTP/2 去 parallel 拉 polyfill assets 速度上升。

我認為複雜且多層次的設計,其實背後代表著對於 User 的廣度、深度、效能上的競賽

如同副標題所說的,若需要支援的 Browser 多、又需要顧及每一 User 的體驗、一點速度都不能掉,那麼自然在一件 “直接載入就完事了” 的問題上就需要深度的探討。

僅支援 modern browser 與需要小量 Polyfill 的客群,自然是選擇法一。

極端的 Case 下,我認為法二與法三同時 implement 進去完全沒有問題,站在 “一點 User 都不能少” 的前提下,付出一點 Architecture Design 的 Effort 與 Client-side 低成本的 feature detection fallback, 我認為可能是相當值得的。

最後,常常有人說前端開發只是調調樣式、碰不到架構設計,你瞧這麼一個聽起來簡單到不行的小問題(多數人往往會一兩句話回答),背後是不是滿滿的設計與考量呢!

If any interest, 👉 https://realdennis.me.